/* Visually hidden but exposed to assistive technology — the standard
   clip-rect pattern. Used by the off-screen model-derived accessibility tree
   (#a11yDocument, docs/67 row 9). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* WCAG 2.4.1 (Level A). Arriving on the page, or returning from the ribbon,
   used to mean tabbing past Search, Save, Properties, Settings, eight menus,
   five ribbon tabs and the whole Home band — roughly 150 controls — before
   reaching the document (docs/104 HF-074). #pages is already tabindex=0, so it
   is a real tab target for the link to hand over to. Off-screen until focused,
   then placed over the chrome so it is visibly the first stop. */
/* Hidden by CLIPPING, not by being parked 9,999px to the left. The old trick
   made the page 11,279px wide the moment the interface went right-to-left —
   `left: -9999px` measures from the RTL origin, so the document grew leftwards
   and opened scrolled away from itself: an Arabic editor that painted a blank
   window. Clipping takes no space in either direction (docs/124 §3.5). */
.skip-link {
  position: absolute;
  top: 0;
  inset-inline-start: 0;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  z-index: 100;
}

.skip-link:focus {
  inset-inline-start: 8px;
  top: 8px;
  width: auto;
  height: auto;
  overflow: visible;
  clip-path: none;
  padding: 8px 12px;
  border-radius: 6px;
  background: var(--surface);
  color: var(--ink);
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* The DOCUMENT is not the interface (docs/124 §3.5).
   When the chrome goes right-to-left for an Arabic or Hebrew reader, these
   surfaces do not follow: a left-to-right `.docx` is left-to-right whoever
   opens it, and inheriting `rtl` here would reverse its punctuation, its list
   markers, its table column order and the caret's idea of "next". Direction
   inside a document belongs to that document's own `w:bidi`, applied per
   paragraph by the renderer — never to the reader's locale. */
.page-wrap,
.page,
.ruler,
.pages-panel {
  direction: ltr;
}

/* ---- Color contract (docs/104 theme T-08) ------------------------------------
   Colour is defined in exactly two kinds of place, and nowhere else:

     1. THIS theme-independent block — geometry, elevation, the settable accent,
        and the "paper layer" (see below), none of which change with the theme.
     2. The three palette blocks that follow — the light block and BOTH dark
        entry points. Every semantic colour token is declared in all three.

   Feature CSS below never writes a bare hex. A literal in a rule is a colour
   with no dark value, which is how tracked-change text ended up at 2.4:1 on the
   dark surface: the review chrome was written in light-mode Google hexes and
   the dark blocks had no idea it existed. `tests/style_tokens.test.mjs` fails
   the build on a bare hex outside these blocks, and on a token that is declared
   in the light block but missing from either dark block — the duplication
   between the two dark blocks is unavoidable in plain CSS (one lives inside a
   media query), so the test is what keeps them in step. */
:root {
  /* Redesign default accent — Studio blue. main.js also sets --accent inline
     from persisted Settings, so this is the fallback/first-paint value.
     `--accent` is the FILL: it keeps the user's chosen value in both themes,
     because accent fills are read against `--accent-ink`, not against the
     theme surface. `--accent-text` (per-theme, below) is the same accent as
     foreground/edge on a theme surface, where a dark blue on a dark panel is
     unreadable — use that one for `color:`, `border-color:` and `outline:`. */
  --accent: #3355c4;
  --accent-strong: color-mix(in srgb, var(--accent) 82%, var(--ink));
  --accent-ink: #ffffff;
  --accent-soft: color-mix(in srgb, var(--accent) 13%, transparent);
  --accent-line: color-mix(in srgb, var(--accent) 42%, transparent);

  /* ---- Paper layer ----------------------------------------------------------
     The document sheet is white in BOTH themes — the editor themes its chrome,
     not the page being authored. Anything painted onto or over the raster (the
     caret, the IME preedit, crop handles, the review markers on the glyphs)
     therefore has a fixed contrast partner and must NOT follow the theme: a
     dark-mode green marker would be drawn onto white paper at ~2:1. These are
     the paper's own colours, and they are deliberately theme-independent. */
  --paper: #ffffff;
  --paper-ink: #111318;
  --paper-insert: #188038;
  --paper-delete: #d93025;
  --paper-comment: #c58b00;
  /* The spelling squiggle. Declared HERE rather than in the three theme blocks
     (as docs/114 §5.7 proposed) for the reason stated above: it is painted onto
     the white sheet in both themes, so a dark-theme value would put a pale red
     on white paper. It shares its value with --paper-delete because both are
     "the platform's red on paper" — but it is its own token, because a squiggle
     and a tracked deletion are not the same thing and one may move. */
  --paper-spelling: #d93025;
  /* The grammar mark. A different colour from the spelling one because
     Word and Docs both distinguish them, and a reader has to be able to
     tell "this word is misspelled" from "this sentence is wrong" without
     opening a menu. Blue on white paper, in the paper layer for the same
     reason as its neighbours. */
  --paper-grammar: #1a73e8;
  /* THE REFLOW SURFACE (docs/151 §6.2, corrected). In reflow there
     is no page and therefore no desk: one background fills the writing surface
     edge to edge and the measure is simply where the text is. Google renames
     the control for exactly this reason — in Pages it is *Page color*, in
     Pageless it is *Background color*, because there is one region rather than
     two (support.google.com/docs/answer/10296604, first-party).

     IT IS A MEMBER OF THE PAPER LAYER, NOT OF THE CHROME PALETTE, and that is
     the load-bearing part rather than an implementation detail. The engine
     fills a tile's raster with the document's own `w:background` — white by
     default (`Surface::with_background`, `casual-doc-render`) — so the surface
     the CSS paints has to be the same colour as the bitmap laid on top of it or
     a seam appears at every tile edge. And everything painted ONTO that raster
     (the caret, the IME preedit, the review markers, the fold chevron, the four
     `--paper-*` marks above) is calibrated against white for the reason the
     comment above gives. Declaring this as `var(--paper)` means the surface
     cannot drift away from the ink that is drawn on it: carry one and the other
     comes with it. `tests/style_tokens.test.mjs` asserts the two resolve equal.

     So a reader on a dark theme gets a WHITE reflow surface, deliberately —
     the chrome is themed, the document is not. A dark reading
     surface is a real want and a separate piece of work, because it needs an
     ink rule for author colours before it can be safe; ONLYOFFICE's HSL
     lightness flip (`darkModeCorrectColor2`) is the prior art to study. Do not
     redefine this token in a theme block without that rule. */
  --paper-surface: var(--paper);
  /* The document scroller's thumb, for when that scroller's track IS the paper
     surface. This is the first thing the uniform surface broke and it is the
     same defect the paper comment above was written about, one surface out:
     `--scrollbar-thumb` is a CHROME token, so in dark theme it is
     `rgba(232, 234, 237, 0.28)` — a near-white translucent thumb, which over a
     white track is a scrollbar you cannot see. Same figures as the LIGHT
     theme's, because the ground is the same near-white; held to that by
     `tests/style_tokens.test.mjs` rather than by indirection, so the chrome
     palette is not made to depend on the paper layer to say it. */
  --paper-scrollbar-thumb: rgba(74, 78, 85, 0.3);
  --paper-scrollbar-thumb-hover: rgba(74, 78, 85, 0.52);

  /* ---- Stacking ladder ------------------------------------------------------
     One ordered list for every body-level floating surface, so "what paints
     over what" is answered here instead of by 27 scattered z-index values.
     A modal is the top of the ladder by definition: while one is open it owns
     the interaction, so nothing else may paint over it (docs/104 HF-089 — the
     pinned tracked-change card used to float above the scrim with live
     Accept/Reject buttons). Surfaces INSIDE the viewport (rulers, page chrome,
     running-band markers) form their own local stacking contexts and keep their
     small local values; they are not part of this ladder. */
  --z-bar: 30;
  --z-selection-toolbar: 40;
  --z-link-chip: 45;
  --z-drawer: 45;
  --z-find: 55;
  --z-menu: 60;
  --z-submenu: 61;
  --z-paste-options: 62;
  --z-inspector: 65;
  --z-review-popover: 66;
  --z-review-card: 67;
  --z-palette: 90;
  /* The touch loupe. Above the floating selection bar and the link chip, which
     it is meant to be read over, and below the modal layer: a dialog is the
     only thing in front of the document and the magnifier is not an exception
     to that (`105` UX-018). */
  --z-magnifier: 95;
  --z-modal: 100;
  /* Above the modal, for the same reason the tooltip below is: the toast is a
     `pointer-events: none` card that never receives input, so it cannot let a
     user act on chrome a modal has taken control of — and a refusal raised BY a
     dialog (a page-setup change Suggesting cannot track) has to be readable
     while that dialog is still on screen. */
  --z-toast: 110;
  /* Above the modal on purpose, and the only thing that is: a tooltip is a
     `pointer-events: none` hint that never receives input, so it cannot let a
     user act on chrome the modal has taken control of. */
  --z-tooltip: 1000;

  /* Rounded shell, from the owner's prototype (Downloads/preview (1).html).
     This SUPERSEDES the earlier near-square pass recorded in docs/63: the shell
     reads as softly rounded rather than flat-gridded. 6px on controls keeps a
     34px button in proportion; 8px on surfaces. Floating popovers keep a small radius so they still
     detach from the surface (and #outlinePanel stays 10px, asserted by
     shell-reference-polish.spec). */
  --radius: 8px;
  --radius-sm: 6px;
  --radius-md: 8px;
  /* Floating surfaces keep their own radius so they detach from the flat
     canvas — and both are asserted by tests (#outlinePanel 10px in
     shell-reference-polish; dialog cards 12px in header-menu). */
  --radius-popover: 10px;
  --radius-dialog: 12px;
  --shadow-popover: 0 10px 34px rgba(20, 22, 28, 0.24);
  --shadow-lg: 0 18px 48px rgba(20, 22, 28, 0.28);
  /* Two layers, not one 30%-opacity slab: a tight contact shadow that seats the
     card, plus a wide soft one for depth. The single heavy shadow read as a
     drop-shadowed box from a decade ago, which is most of why the dialogs felt
     heavier than the chrome around them. */
  --shadow-dialog: 0 1px 2px rgba(20, 22, 28, 0.12), 0 16px 48px rgba(20, 22, 28, 0.18);
  /* Named type, control-height, and icon grids from doc 69 §3.3/§4.1/§4.2.
     Values preserve the existing shell except where the design explicitly
     consolidates equivalent caption/icon roles. */
  --fs-micro: 9px;
  --fs-caption: 10px;
  --fs-small: 12px;
  --fs-body: 13px;
  --fs-medium: 15px;
  --fs-title: 16px;
  --fs-ribbon-caption: 10.5px;
  --fs-ribbon-tab: 13.5px;
  --h-control: 30px;
  --h-search: 34px;
  --h-field: 36px;
  /* The ribbon tab strip, now that it shares the header with the document
     title. 28px is ONLYOFFICE's `@toolbar-height-tabs-top-title`
     (`apps/common/main/resources/less/variables.less:104`) — the height they
     use for exactly this arrangement, against 32px for a tab strip on a row of
     its own. Ours was 34px on a row of its own; see docs/123 §3. */
  --h-tab: 28px;
  --h-rail-control: 50px;
  --h-footer: 30px;
  /* The band above the footer that the status toast owns, and that a
     narrow-screen DRAWER therefore may not extend into (`109` HF-233).
     The toast is pinned to the viewport's bottom-start corner and a drawer is
     pinned to the whole right-hand side, so below the drawer breakpoint the two
     fixed surfaces share the bottom edge and nothing arbitrates between them —
     the toast lands on the drawer's footer ACTION, which is the one thing there
     that a reader has to be able to see and press. Reserving the band is the
     arbitration, and it is one rule for every drawer rather than a z-index
     argument per panel: the toast keeps the corner, the drawer stops above it.

     Reserved UNCONDITIONALLY rather than only while a toast is up, because a
     drawer that grew and shrank as announcements came and went would move the
     control under the reader's finger — a jump is worse than 56px.

     56px is three lines of `--fs-small` at 1.45 plus the card's own padding and
     border, which is the tallest a refusal gets before it wraps past what a
     transient card should carry at all. A toast taller than that still grows
     upward over the drawer's LIST, which is scrollable content and not a
     control; the guarantee the guard holds is about the footer action. */
  --h-toast-band: 56px;
  /* The header is two bands now (see `.bar`): the document's title row over the
     navigation row. 26 + 28 + 5 + 4 of padding = 63. It was 58 while the two
     rows measured 24 + 28 + 9 = 61, so the chrome overflowed its own bar by 3px
     and `align-items: center` hid the fact by splitting the difference. */
  --h-header-title: 26px;
  --h-header: 63px;
  /* The ribbon band. Content-sized in reality, so this number has to be kept
     honest by a test rather than by arithmetic; responsive-shell.spec.mjs
     asserts the panels below actually clear the ribbon's measured bottom edge.
     It was 92px while every group carried an uppercase caption under it;
     deleting that row (docs/123 §4 — ONLYOFFICE's groups have no captions at
     all) took the tallest band, Home, down to ~72px, which is within a couple
     of pixels of their own 66px `@toolbar-height-controls`. */
  --h-ribbon-band: 90px;
  /* The compact toolbar's height — the prototype's --toolbar-h. Declared here
     so `--chrome-bottom` and the bar itself read the same number. */
  --h-compact-bar: clamp(38px, 3.4vh, 46px);

  /* Chrome card: the ribbon band and the compact bar are the same object at
     different densities, so they share one radius and one inset. Both scale —
     a fixed 16px reads heavy on a small window and mean on a large one. */
  --chrome-card-radius: clamp(10px, 1.1vw, 18px);
  --chrome-card-inset-y: clamp(1px, 0.25vh, 4px);
  /* ONE height for every ribbon band. It used to be whatever the tallest group
     on the active tab happened to be — Home 79px (two control rows), Insert /
     Layout / References 58px, Review / View 48px — so changing tab moved the
     document up to 31px under the reader's cursor. Word and ONLYOFFICE both pin
     the band and let the groups align inside it.
     This is the two-control-row height the Home band needs, and it is a MINIMUM
     rather than a fixed height on purpose: if Home ever grows a taller control,
     the band grows with it and `ribbon-band-height.spec.mjs` fails until this
     token is re-measured, rather than Home silently clipping. */
  --ribbon-band-h: 90px;
  /* The card inset is CONTENT WIDTH the band no longer has. The Home band needs
     1254px of groups at 1280px, so the whole chrome overhead — this inset plus
     the panel's own padding — has to stay under 26px or the overflow pass
     exiles a group. The first version of this token cost 15.4px on its own and
     put the band 9px over, which is how Styles ended up in the "⋯" menu on an
     ordinary 1280px desktop. Keep it small at desktop widths and let it grow
     only where there is width to spare. `webapp/tests/e2e/ribbon-home.spec.mjs`
     guards the budget. */
  --chrome-card-inset-x: clamp(2px, 0.3vw, 10px);

  /* Dialog rhythm. One scale for every dialog and panel, so tightening the
     density is one edit rather than thirty. The old values were a flat 18-20px
     on head, body and foot alike, which made every dialog tall without making
     any of it more legible. */
  /* Dialog rhythm. These were declared and referenced NOWHERE — every dialog
     hardcoded `18px var(--space-5) 14px` instead — so the responsive padding
     the design called for never reached a pixel. They are wired into
     .dialog-head/.dialog-body/.dialog-foot below, and `dialog_density`
     fails if a dialog goes back to hardcoding its chrome padding. */
  --dlg-pad-x: clamp(14px, 2.2vw, 22px);
  --dlg-pad-y: clamp(10px, 1.4vh, 16px);
  /* PANEL rhythm — the same mechanism as the dialog pair above, for the side
     panels, which had no shared shell at all.
   *
     MEASURED, 2026-10-05, Chromium at 1280x720 and 1440x900, on this branch's
     parent: `.panel-head` was `padding: 0 8px 0 14px` and `.panel-body` was
     `padding: 6px`, so head and body disagreed by 8px in EVERY side panel —
     Outline's title sat at x=101.09 over a body whose content box started at
     93.09, Compare's title at 101.09 over its first paragraph at 93.09, Version
     History's title at 967 over a body at 959. Five panels then overrode even
     those (`.glyph-body` 10px, `.version-list` 4px 6px, the two properties
     bodies 13px 14px 16px), so there were four insets in a family of nine
     surfaces and the head/body step was different in each.
   *
     The values are not invented: they are what `.find-panel-head` and
     `.find-panel-body` already used — `var(--space-2) var(--space-4)` — and the
     find panel was the ONE panel whose head and body agreed. One pair, declared
     here, is why 23 dialogs have 23 identical left edges; the panels now take
     their inset the same way. `dialog_density.test.mjs` fails if a panel goes
     back to hardcoding its chrome padding, and
     `e2e/panel-alignment.spec.mjs` measures the result. */
  --pnl-pad-x: var(--space-4);
  --pnl-pad-y: var(--space-2);
  /* The File page's reading measure (HF-265 D4). The pane is 960px wide at
     1280 and 1600px at 1920 — it is a two-column backstage, not a card — and
     the content inside it was capped at a flat 760px while the HEADING over
     that content was not capped at all. Measured: the heading ran 152px past
     its own column at 1280 and 792px past it at 1920, which is a title that
     stops looking like it belongs to the thing under it.

     The fix is the measure, not the alignment: the heading joins the column
     rather than the column chasing the heading. Centring a 760px column inside
     a pane that is CHOSEN FROM A LEFT NAV would put the content nowhere near
     the row that opened it (ONLYOFFICE's backstage, Word's File page and Google
     Docs' settings all left-align a measured column under a heading of the same
     width), and letting the column grow to 1600px is the "a four-field dialog as
     wide as a twenty-field one" defect this stylesheet already names. One token,
     read by the heading, the blurb and the pane, so the three cannot drift. */
  --file-page-measure: 760px;
  /* Bottom edge of the fixed shell chrome: header bar (which now carries the
     tab strip) + ribbon band. Anything that floats over the workarea from a viewport-fixed
     position anchors to THIS, never to a literal, so the ribbon's right-hand
     group and its "⋯" overflow button can never end up underneath a panel
     (HF-084). Redefined below for the collapsed ribbon, where the band is gone. */
  --chrome-bottom: calc(var(--h-header) + var(--h-ribbon-band));
  --icon-compact: 16px;
  --icon-control: 18px;
  --icon-rail: 20px;
  /* The dialog width scale. Four rungs, and the fourth is here because
     `.settings-dialog` had already invented it in raw pixels (960px) while
     `--dialog-width-wide` was declared and referenced nowhere — a scale with a
     dead rung and an undeclared one is not a scale. `dialog_density.test.mjs`
     now fails on ANY unused token in this family rather than on a two-name
     list, which is what let the dead rung sit here. */
  --dialog-width-compact: 380px;
  --dialog-width-standard: 640px;
  --dialog-width-wide: 760px;
  --dialog-width-xwide: 960px;
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
}

/* Light palette (default + explicit). */
:root,
:root[data-theme="light"] {
  /* Warm "Studio" light palette from the approved editor redesign: paper-white
     canvas (#FBFAF8), quiet stone surfaces (#F2F1ED), warm-grey hairlines
     (#EDEBE6 / #DEDBD4), and graphite ink (#14161A). Everything downstream is
     tokenized, so this single block re-skins the whole chrome. */
  /* Google-Docs-family cool greys, from the owner's prototype. The previous
     scale was a WARM cream (#fbfaf8 / #f2f1ed / #edebe6) which read bright and
     flat and looked nothing like Docs.
     Regions separate by SHADE, not by hairline: canvas is the darkest, chrome
     and panels are white on top of it, and the page is white against the darker
     canvas so it reads as paper. Borders are a last resort, not the mechanism. */
  --bg: #eef1f7;         /* app chrome strip — the header/toolbar band */
  --bg-2: #f4f6fb;       /* sunken panes and the toolbar pill */
  --surface: #ffffff;    /* panels, dialogs, dropdowns, the page */
  --surface-alt: #f4f6fb;
  --ink: #1f1f1f;
  /* Like their dark counterparts, --muted and --faint are pinned to clear WCAG
     AA (4.5:1) against the darkest surface they are painted on — here --bg-2,
     not --surface. --faint was #6f747c, which passes on white (4.70:1) and fails
     on --bg-2 (4.16:1), so the search box's "⌘⇧P" hint and the footer's "Mode"
     label were the two places it showed. Guarded by theme-contrast.spec.mjs. */
  --muted: #5f6368;
  /* --faint was #8a8e95 = 3.29:1 on --surface, below the 4.5:1 body-text floor,
     and it is the colour of every menu shortcut hint and empty state — the prose
     a first-time user needs most (docs/104 HF-093). #6f747c measures 4.70:1 on
     --surface and 4.51:1 on --bg. */
  /* #686b70 is the LIGHTEST value that still clears AA (4.5:1) on the darkest
     surface it is painted on — the #eceef1 canvas, where it measures 4.60:1.
     The prototype's own #747775 measures 3.90:1 there and would fail. */
  --faint: #686b70;
  --line: #e3e8f1;
  --line-strong: #cfd6e4;
  --scrollbar-thumb: rgba(74, 78, 85, 0.3);
  --scrollbar-thumb-hover: rgba(74, 78, 85, 0.52);
  --shadow-sm: 0 1px 2px rgba(20, 22, 26, 0.05);
  --shadow-page: 0 1px 3px rgba(20, 22, 26, 0.12), 0 10px 30px rgba(20, 22, 26, 0.09);
  /* Graphite primary action (redesign): the mock's "Open" / "Save .docx" /
     "Apply" buttons are solid ink, not the tinted accent. Kept theme-aware so
     dark mode gets an inverse (light) primary instead of an unreadable dark one. */
  --primary-bg: #14161a;
  --primary-bg-hover: #2c2f35;
  --primary-ink: #fbfaf8;
  /* The accent as foreground/edge on a theme surface. Identical to the fill in
     light mode; the dark blocks mix it toward white so a user-chosen accent
     cannot land unreadable on a dark panel however dark they picked it. */
  --accent-text: var(--accent);
  /* Semantic status colours. `-line` is the border/edge weight (3:1 UI bar),
     `-soft` the tinted background. Measured against --surface. */
  --success: #137333;
  --success-line: #188038;
  --success-soft: color-mix(in srgb, var(--success-line) 12%, var(--surface));
  --warning: #b06000;
  --warning-line: #f9ab00;
  --warning-soft: color-mix(in srgb, var(--warning-line) 16%, var(--surface));
  --danger: #b3261e;
  --danger-line: #d93025;
  --danger-soft: color-mix(in srgb, var(--danger-line) 12%, var(--surface));
  --info: #1a56c4;
  --info-line: #1a73e8;
  --info-soft: color-mix(in srgb, var(--info-line) 12%, var(--surface));
  /* Review roles. Insertion/deletion/replacement reuse the status ramp so a
     tracked change reads the same as any other affirmative/destructive signal;
     move and formatting need their own hues to stay distinguishable from it. */
  --review-insert: var(--success);
  --review-insert-line: var(--success-line);
  --review-insert-soft: var(--success-soft);
  --review-delete: var(--danger);
  --review-delete-line: var(--danger-line);
  --review-delete-soft: var(--danger-soft);
  --review-replace-line: var(--info-line);
  --review-format-line: #9334e6;
  --review-move: #0b8043;
  color-scheme: light;
}

/* Dark palette — via system pref (only when no explicit theme) or explicit
   choice.

   `--muted` and `--faint` are pinned to clear WCAG AA (4.5:1) against
   `--surface-alt`, the darkest surface either one is painted on. They used to be
   #969ba3/#8b9199, which measured 4.42:1 under the ribbon's group captions —
   UNDO, CLIPBOARD, FONT, PARAGRAPH, STYLES, EDITING, MODE. Nine-pixel text
   failing AA is not a rounding error: those captions are the smallest type in
   the product and they were the labels telling you what the toolbar groups are.
   The light palette passed, so only dark was affected, which is why it read as
   "dark mode is broken in the toolbar" rather than as a palette bug.
   `tests/e2e/theme-contrast.spec.mjs` measures this on the live ribbon in both
   themes, so re-darkening either token fails the build.

   The two blocks below are byte-identical on purpose and MUST stay so:
   a user who picks Dark on a light OS gets only the second one, which is how
   half the theme used to be missing for them (docs/104 HF-092). Plain CSS
   cannot share one declaration list across a media query and a bare selector,
   so `tests/style_tokens.test.mjs` asserts the two agree instead. Add a token
   to one and the test fails until it is in the other, and in the light block. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
    --bg: #141619;
    --bg-2: #191c20;
    --surface: #212429;
    --surface-alt: #282c31;
    --ink: #e8eaed;
    --muted: #a8aeb6;
    --faint: #9aa0a8;
    --line: #2e323a;
    --line-strong: #383d46;
    --scrollbar-thumb: rgba(232, 234, 237, 0.28);
    --scrollbar-thumb-hover: rgba(232, 234, 237, 0.5);
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
    --shadow-page: 0 1px 3px rgba(0, 0, 0, 0.4), 0 10px 30px rgba(0, 0, 0, 0.35);
    --primary-bg: #e8eaed;
    --primary-bg-hover: #ffffff;
    --primary-ink: #16181c;
    --accent-text: color-mix(in srgb, var(--accent) 58%, #ffffff);
    --success: #81c995;
    --success-line: #5bb974;
    --success-soft: color-mix(in srgb, var(--success-line) 20%, var(--surface));
    --warning: #fdd663;
    --warning-line: #e0b25c;
    --warning-soft: color-mix(in srgb, var(--warning-line) 20%, var(--surface));
    --danger: #f28b82;
    --danger-line: #ee675c;
    --danger-soft: color-mix(in srgb, var(--danger-line) 20%, var(--surface));
    --info: #8ab4f8;
    --info-line: #669df6;
    --info-soft: color-mix(in srgb, var(--info-line) 20%, var(--surface));
    --review-insert: var(--success);
    --review-insert-line: var(--success-line);
    --review-insert-soft: var(--success-soft);
    --review-delete: var(--danger);
    --review-delete-line: var(--danger-line);
    --review-delete-soft: var(--danger-soft);
    --review-replace-line: var(--info-line);
    --review-format-line: #d7aefb;
    --review-move: #6dd58c;
    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --bg: #141619;
  --bg-2: #191c20;
  --surface: #212429;
  --surface-alt: #282c31;
  --ink: #e8eaed;
  --muted: #a8aeb6;
  --faint: #9aa0a8;
  --line: #2e323a;
  --line-strong: #383d46;
  --scrollbar-thumb: rgba(232, 234, 237, 0.28);
  --scrollbar-thumb-hover: rgba(232, 234, 237, 0.5);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
  --shadow-page: 0 1px 3px rgba(0, 0, 0, 0.4), 0 10px 30px rgba(0, 0, 0, 0.35);
  --primary-bg: #e8eaed;
  --primary-bg-hover: #ffffff;
  --primary-ink: #16181c;
  --accent-text: color-mix(in srgb, var(--accent) 58%, #ffffff);
  --success: #81c995;
  --success-line: #5bb974;
  --success-soft: color-mix(in srgb, var(--success-line) 20%, var(--surface));
  --warning: #fdd663;
  --warning-line: #e0b25c;
  --warning-soft: color-mix(in srgb, var(--warning-line) 20%, var(--surface));
  --danger: #f28b82;
  --danger-line: #ee675c;
  --danger-soft: color-mix(in srgb, var(--danger-line) 20%, var(--surface));
  --info: #8ab4f8;
  --info-line: #669df6;
  --info-soft: color-mix(in srgb, var(--info-line) 20%, var(--surface));
  --review-insert: var(--success);
  --review-insert-line: var(--success-line);
  --review-insert-soft: var(--success-soft);
  --review-delete: var(--danger);
  --review-delete-line: var(--danger-line);
  --review-delete-soft: var(--danger-soft);
  --review-replace-line: var(--info-line);
  --review-format-line: #d7aefb;
  --review-move: #6dd58c;
  color-scheme: dark;
}

* {
  box-sizing: border-box;
}

/* Baseline keyboard-focus ring (docs/104 HF-067). Anything the chrome styles
   explicitly still wins on specificity; this only guarantees that a control
   nobody remembered to style is never focused invisibly. Deliberately
   `:focus-visible`, so a pointer click does not paint a ring. */
:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

/* ---- Scrollbars: one thin, quiet, overlay-style treatment everywhere --------
   Per the doc 63 design system, every `overflow:auto` surface (the single
   document scrollbar on `.viewport`, plus dialog bodies, menus, the command
   list, and side panels) shares one minimal bar: a transparent track and a
   translucent thumb that darkens on hover. Applied globally so no default
   chunky OS bar leaks through, then reinforced on the known scroll surfaces. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) transparent;
}
::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border: 2px solid transparent;
  border-radius: 8px;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
  background: var(--scrollbar-thumb-hover);
  background-clip: padding-box;
}
::-webkit-scrollbar-corner {
  background: transparent;
}

html,
body {
  margin: 0;
  height: 100%;
  /* The shell is a FRAME, not a page. Nothing here overflows — the header, the
     band, the work area and the status bar are a flex column that fits, and
     `#viewport` does all the scrolling — but `overscroll-behavior: auto` still
     lets the browser rubber-band the ROOT when a gesture starts over a part
     that cannot scroll. The owner's report: dragging on the header or the
     toolbar moved the whole frame and it sprang back on release. Nothing that
     is not a scroller should move under a scroll. */
  overscroll-behavior: none;
}

body {
  display: flex;
  flex-direction: column;
  /* `--brand-font` is a white-label seam (docs/126 phase 3): a host names a face
     they placed NEXT TO the editor and `src/brand.css` declares an @font-face for
     it. Never a CDN — `src/fonts.css` self-hosts Inter and Material Symbols and
     `tests/chrome_fonts.test.mjs` asserts no remote font reference, because the
     chrome has to paint with the network off. Our own stack stays behind it, so a
     face that fails to load leaves readable chrome rather than a system serif. */
  font:
    var(--fs-body)/1.5 var(--brand-font, "Inter"), -apple-system, BlinkMacSystemFont, "SF Pro Text",
    "Segoe UI", Roboto, sans-serif;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

button,
select,
input {
  font: inherit;
}

/* ---- Header bar ------------------------------------------------------------ */
/* TWO BANDS, Google Docs' arrangement: row 1 is the document (title + state),
   row 2 is the application (navigation + how it is presented). Word splits the
   same way — a title bar over the ribbon row, with Ribbon Display Options on
   the ribbon row, not beside the filename.
   It was one flex row, so `.controls` centred itself over BOTH bands at once
   and the state chip, the toolbar-mode toggle, the finding count and the two
   icon buttons all landed on a line that belonged to neither. The grid puts
   each control in the band that owns it, and `.document-chrome` / `.controls`
   are `display: contents` so their children place into these rows directly. */
.bar {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-rows: var(--h-header-title) var(--h-tab);
  grid-template-areas:
    "brand title docstate"
    "brand nav   apptools";
  align-items: center;
  column-gap: 12px;
  height: var(--h-header);
  padding: 5px 14px 4px;
  /* Same grey as the toolbar, status bar and desk — one field, separated from
     the page by SHADE rather than by a rule. */
  background: var(--bg);
  border-bottom: 0;
  box-shadow: none;
  z-index: var(--z-bar);
}

/* Visible ⌘K entry point (doc 69 §1.4.1/§1.2) — the palette itself already
   existed and worked; it only lacked a discoverable trigger. Wired to the
   same openCmd() the keyboard shortcut calls, in main.js. */
.search-trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  height: var(--h-search);
  padding: 0 10px;
  margin-left: 6px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--muted);
  cursor: pointer;
  flex: 0 1 240px;
  min-width: 0;
}

.search-trigger:hover {
  background: var(--bg-2);
  border-color: var(--muted);
}

.search-trigger .ms {
  flex: 0 0 auto;
  font-size: var(--icon-compact);
}

.search-trigger .search-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  font-size: var(--fs-body);
}

.search-trigger kbd {
  flex: 0 0 auto;
  font: 600 var(--fs-caption)/1 inherit;
  color: var(--faint);
  background: var(--bg-2);
  border-radius: 4px;
  padding: 2px 6px;
}

.brand {
  grid-area: brand;
  display: flex;
  align-items: center;
  gap: 9px;
  flex: 0 0 auto;
  min-width: 0;
}

.brand .mark {
  font-weight: 700;
  font-size: var(--fs-medium);
  letter-spacing: -0.02em;
}

/* The brand mark. Painted from a TOKEN rather than from an `<img src>`, so a
   white-labelled deployment changes it in `brand.json` and regenerates
   `src/brand.css` — no markup edit, which is what `docs/126` phase 3's "no code
   changes" gate means in practice. The `var()` carries our own mark as its
   fallback so a deployment WITHOUT `brand.css` still shows a mark rather than an
   empty box, and so `style.css` does not read a token it never defines
   (`tests/style_tokens.test.mjs`). */
/* The mark as a link back to the host's product (ONLYOFFICE
   `customization.logo.url`). Generated only when `brand.json` names a `markHref`,
   and it carries the product name in `.sr-only` text rather than on an
   `aria-hidden` wrapper — a focusable node inside `aria-hidden` is the axe
   violation the repository forbids outright. */
.brand-logo-link {
  display: block;
  line-height: 0;
  border-radius: var(--radius);
}

.brand-logo-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.brand-logo {
  display: block;
  width: 26px;
  height: 26px;
  background-image: var(--brand-mark, url("../opendoc-mark.svg"));
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}

/* Both wrappers dissolve into `.bar`'s grid: each child places itself into the
   band that owns it. They stay in the DOM because scripts and the embedding
   classes address them by class. */
.controls,
.document-chrome {
  display: contents;
}

.controls-doc,
.controls-app {
  display: flex;
  align-items: center;
  gap: 8px;
  justify-self: end;
  min-width: 0;
}

.controls-doc {
  grid-area: docstate;
}

.controls-app {
  grid-area: apptools;
}

/* `[hidden]` has to beat `display: contents`, and it has to take the children
   with it — which `display: none` on the contents box does. */
.document-chrome[hidden] {
  display: none;
}

.document-title-row {
  grid-area: title;
  display: flex;
  align-items: center;
  min-width: 0;
  height: var(--h-header-title);
  gap: 7px;
}

.document-state {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  gap: 4px;
  padding: 1px 7px 1px 5px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--surface-alt);
  color: var(--muted);
  font-size: var(--fs-small);
  white-space: nowrap;
}

.document-state .ms {
  font-size: var(--icon-compact);
}

.document-state[data-state="edited"] {
  color: var(--accent-strong);
}

.document-state[data-state="downloaded"] {
  color: var(--success);
}

/* The bar scrolls horizontally with its scrollbar suppressed, so when the
   header ran out of room the trailing menus (Tools, Help) were cut off mid-row
   with no fade, no chevron and no bar — they simply were not there, and nothing
   said they existed (HF-097). The trailing mask makes the clip legible: it
   fades whatever is cut instead of slicing it, which is the standard "there is
   more this way" cue for a hidden-scrollbar strip. It costs no height, which
   matters because the 58px header band is fully committed — a visible scrollbar
   would have to come out of the menu row itself.
   It is self-revealing rather than always-on: the bar is a flex box that fills
   `.document-chrome`, so when every menu fits, the last 22px are empty and the
   fade falls on nothing. This is the affordance half only; collecting the
   overflow into a "⋯" button the way the ribbon does needs the measuring pass
   in main.js (`updateRibbonOverflow`), which is not a stylesheet change. */
.app-menu-bar {
  grid-area: nav;
  display: flex;
  align-items: center;
  min-width: 0;
  height: 21px;
  margin-left: 1px;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-mask-image: linear-gradient(to right, black calc(100% - 22px), transparent);
  mask-image: linear-gradient(to right, black calc(100% - 22px), transparent);
}

.app-menu-bar::-webkit-scrollbar {
  display: none;
}

.app-menu-button {
  flex: 0 0 auto;
  height: 21px;
  padding: 1px 7px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--ink);
  font-size: var(--fs-body);
  line-height: 19px;
  cursor: pointer;
}

.app-menu-button:hover,
.app-menu-button[aria-expanded="true"] {
  background: var(--bg-2);
}

/* HF-067: hover and keyboard focus used to share this one 1.13:1 background
   change, so arrowing across File/Edit/View — the app's primary command
   surface — showed nothing at all. Focus is a ring, hover is a tint; they are
   different states and now read as different states. */
.app-menu-button:focus-visible {
  background: var(--bg-2);
  outline: 2px solid var(--accent-text);
  outline-offset: -1px;
}

/* ---- Buttons --------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: var(--h-control);
  padding: 0 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--ink);
  font-weight: 550;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.12s, border-color 0.12s, opacity 0.12s;
}

/* Unified icon grid (doc 69 §4.2): header/menu icons are 18px, matching
   .fmt's ribbon icons instead of a separate 17px size. */
.btn .ms {
  font-size: var(--icon-control);
}

.btn:hover:not(:disabled) {
  background: var(--bg-2);
  border-color: var(--muted);
}

.btn:disabled {
  opacity: 0.45;
  cursor: default;
}

.btn-primary {
  background: var(--primary-bg);
  border-color: var(--primary-bg);
  color: var(--primary-ink);
}

.btn-primary:hover:not(:disabled) {
  background: var(--primary-bg-hover);
  border-color: var(--primary-bg-hover);
}

.btn.icon-only {
  width: 30px;
  padding: 0;
  justify-content: center;
}

.save-format {
  height: var(--h-control);
  max-width: 132px;
  padding: 0 26px 0 9px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
}

.save-format:hover:not(:disabled) {
  background: var(--bg-2);
  border-color: var(--muted);
}

.save-format:disabled {
  opacity: 0.45;
  cursor: default;
}

/* A neutral status CHIP, the same object as `.document-state` beside it — not
   accent-blue prose. As bare blue text with no container this was the loudest
   thing in the chrome ("185 import findings") and read as an unexplained error;
   Word's and Docs' conversion notices are neutral status, and the count is
   reassurance that loss was DETECTED, not a failure. Same shape as its
   neighbour so the two read as one cluster of document state. */
.compatibility-status {
  display: inline-flex;
  align-items: center;
  flex: 0 1 auto;
  max-width: 170px;
  padding: 1px 7px;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--surface-alt);
  overflow: hidden;
  color: var(--muted);
  font-size: var(--fs-small);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* `hidden` is a UA `display: none`, and the author `display` above beats it, so a
   clean document's chip painted as an empty 16x4 bordered box. As a BUTTON that
   would be a focusable control with nothing to say, so the attribute is honoured
   here explicitly. */
.compatibility-status[hidden] {
  display: none;
}

/* It opens the findings, so it reads as something to press. */
button.compatibility-status {
  cursor: pointer;
}

button.compatibility-status:hover {
  border-color: var(--line-strong);
  color: var(--ink);
}

/* The findings dialog (`compat_findings.mjs`): one section per kind of outcome,
   one compact row per finding. A feature id is a technical token and may be long,
   so it wraps anywhere rather than pushing the card wider. */
.findings-group + .findings-group {
  margin-top: var(--space-4);
}

.findings-group-title {
  margin: 0 0 var(--space-2);
  font-size: var(--fs-body);
  font-weight: 600;
}

.findings-total {
  color: var(--muted);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}

.findings-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.findings-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-3);
  align-items: baseline;
  padding: 2px 0;
}

/* What the finding IS, in words (`109` FID-AT-05) — the row's primary text. */
.findings-name {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* The engine's stable id, kept for support and read last: muted and smaller,
   so it is findable without competing with the words. */
.findings-feature {
  min-width: 0;
  color: var(--muted);
  font-size: var(--fs-caption);
  overflow-wrap: anywhere;
}

.findings-count,
.findings-where {
  color: var(--muted);
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
}

.findings-where {
  overflow-wrap: anywhere;
}

.findings-note {
  margin: 0 0 var(--space-2);
  color: var(--muted);
  font-size: var(--fs-small);
}

/* Word's own bookkeeping: a real disclosure inside its kind group, collapsed
   until asked. A full-width row so the whole line is the target; 44px under a
   coarse pointer, the touch floor every other row in this editor keeps. */
.findings-bookkeeping {
  margin-top: var(--space-2);
}

.findings-disclosure {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 28px;
  padding: 2px var(--space-1);
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  font: inherit;
  text-align: start;
  cursor: pointer;
}

.findings-disclosure:hover {
  background: var(--accent-soft);
}

.findings-disclosure:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.findings-disclosure .ms {
  flex: 0 0 auto;
  color: var(--muted);
  font-size: 18px;
  line-height: 1;
}

/* `chevron_right` points the way the row reads; in a right-to-left locale that
   is left. */
.findings-disclosure:dir(rtl)[aria-expanded="false"] .ms {
  transform: scaleX(-1);
}

.findings-bookkeeping-total {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.findings-bookkeeping-panel {
  padding-inline-start: calc(18px + var(--space-2) + var(--space-1));
}

.findings-bookkeeping-panel[hidden] {
  display: none;
}

@media (pointer: coarse) {
  .findings-disclosure {
    min-height: 44px;
  }
}

.file {
  position: relative;
}

.doc-title {
  font: inherit;
  font-size: var(--fs-body);
  font-weight: 550;
  color: var(--ink);
  width: auto;
  max-width: 300px;
  min-width: 0;
  flex: 1 1 auto;
  padding: 3px 6px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: text;
}

.doc-title:hover {
  background: var(--bg-2);
}

.doc-title:focus {
  outline: none;
  cursor: text;
  background: var(--surface);
  border-color: var(--accent-text);
  box-shadow: 0 0 0 2px var(--accent-soft);
  text-overflow: clip;
}


.app-menu-popover {
  position: fixed;
  z-index: var(--z-menu);
  width: min(286px, calc(100vw - 16px));
  max-height: min(560px, calc(100vh - var(--h-header) - 16px));
  padding: 6px;
  overflow-y: auto;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-popover);
  background: var(--surface);
  box-shadow: var(--shadow-popover);
  overscroll-behavior: contain;
}

.app-menu-popover[hidden] {
  display: none;
}

.app-menu-item,
.app-menu-item-parent {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  width: 100%;
  min-height: 31px;
  padding: 5px 9px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}

/* A band folded behind one row. The flyout is positioned against the wrapper,
   which is why the wrapper is the positioned box and not the popover: a flyout
   anchored to the popover would have to know the row's offset, and the row
   already knows it. */
.app-menu-item-submenu {
  position: relative;
}

/* The chevron sits in the row's trailing column — the same slot a shortcut
   occupies, so a menu of mixed rows keeps one right edge. */
.app-menu-item-chevron {
  color: var(--muted);
  font-size: var(--fs-small);
  line-height: 1;
}

.app-menu-item-parent[aria-expanded="true"] {
  background: var(--surface-alt);
}

/* FIXED, not absolute. The popover scrolls (`overflow-y: auto`), and an
   overflow container clips on BOTH axes — so an absolutely positioned flyout at
   `left: 100%` was cut off at the popover's right edge. Measured: the popover
   ended at x=562 and the flyout ran 555..735, so 173px of it simply was not
   painted, while `hidden` was correctly false the whole time. Fixed positioning
   takes it out of that context; `command_menu.mjs` places it against its
   parent row. */
.app-menu-item-flyout {
  position: fixed;
  z-index: var(--z-menu);
  width: max-content;
  min-width: 180px;
  max-width: min(286px, calc(100vw - 16px));
  max-height: min(420px, calc(100vh - var(--h-header) - 16px));
  padding: 6px;
  overflow-y: auto;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-popover);
  background: var(--surface);
  box-shadow: var(--shadow-popover);
  overscroll-behavior: contain;
}

.app-menu-item-flyout[hidden] {
  display: none;
}

/* ON A PHONE A SUBMENU DRILLS, IT DOES NOT FLY OUT.
   The menus are bottom sheets at this width (`docs/148`), and a sideways
   flyout in a sheet is either off-screen or a horizontal scroller — and no
   horizontal scrolling is the property that layout is guarded on. So the
   flyout opens INLINE beneath its parent, indented, which is the same shape a
   phone settings screen uses. */
@media (max-width: 620px) {
  .app-menu-item-flyout {
    position: static;
    width: auto;
    min-width: 0;
    max-width: none;
    max-height: none;
    margin: 2px 0 2px 12px;
    padding: 2px 0 2px 8px;
    border: 0;
    border-left: 1px solid var(--line);
    border-radius: 0;
    box-shadow: none;
    background: transparent;
  }

  .app-menu-item-parent[aria-expanded="true"] .app-menu-item-chevron {
    transform: rotate(90deg);
  }

  .app-menu-item-chevron {
    display: inline-block;
    transition: transform 0.12s ease;
  }
}

.app-menu-item:hover:not(:disabled),
.app-menu-item-parent:hover {
  background: var(--bg-2);
}

.app-menu-item:focus-visible,
.app-menu-item-parent:focus-visible {
  background: var(--accent-soft);
  outline: 2px solid var(--accent-text);
  outline-offset: -2px;
}

.app-menu-item:disabled {
  color: var(--faint);
  cursor: default;
}

.app-menu-item-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-menu-item-hint {
  margin-left: 18px;
  color: var(--muted);
  font-size: var(--fs-small);
  white-space: nowrap;
}

.app-menu-item:disabled .app-menu-item-hint {
  color: var(--faint);
}

/* A band of related rows. It was a `.app-menu-separator` div between runs of
 * rows; it is now the rows' own container, so the band has a name a screen
 * reader can announce and the rule is the container's top edge rather than an
 * element that exists only to be a line (`command_menu.mjs` says why the group
 * won over the separator). Same 1px in the same token — no visual change to
 * the hairline itself, only to what it is drawn ON. */
.app-menu-item-group + .app-menu-item-group {
  margin-top: 5px;
  padding-top: 5px;
  border-top: 1px solid var(--line);
}

/* The compact bar's Table dropdown renders the SAME bands from the SAME
 * declaration, so it gets the same rule. `.menu-group` rather than a
 * table-specific class: the next dropdown built out of `APP_MENU_SECTIONS`
 * inherits it without a second stylesheet block. */
.menu-group + .menu-group {
  margin-top: 4px;
  padding-top: 4px;
  border-top: 1px solid var(--line);
}

/* ---- Settings dialog ------------------------------------------------------ */
/* It was a popover anchored to the header's gear, and it grew past the height
 * of the window as settings were added — spelling and grammar were the last
 * two — so Reset and the autosave controls were unreachable and scrolling it
 * scrolled the document behind. The owner's report: "its height exceed .. so
 * that panel doesnt makes any sense now .. even for compact view .. see dialog
 * instead". Google Docs' Preferences and Word's Options are both modal
 * dialogs. It is one now, in the shared `.dialog-overlay` shell, so it inherits
 * the scrim, the scroll containment and the one modal contract — and the shape
 * the File page already knows how to borrow. */
/* Wide enough for two columns of settings; see `.settings-body`. It was 520px
   and one column, which put 779px of content into 542px of body — the worst
   overflow of any dialog here, and the 237px below the fold held Autosave and
   both proofing toggles.
   Qualified by `.dialog-card` on purpose: that rule is declared LATER in this
   file at the same specificity, so a bare `.settings-dialog { width }` loses to
   its 640px and the card silently comes out narrower than it asks for — which
   is also why the previous value here was expressed as `max-inline-size`. At
   640 the reviewer-identity pair no longer fits side by side and stacks, so
   this is load-bearing rather than cosmetic. */
/* Wide enough for THREE columns, which is what keeps every section above the
   fold.
 *
 * At 720px the `column-width: 17rem` track below fits exactly two columns, so
 * the body's height is half the total content — and when the measurement-units
 * section arrived (105px plus its 20px group margin) that half grew past the
 * card's own box: 571px of content in a 560px body at 1280x720, which
 * `dialog-fit.spec.mjs` fails by 11px. The body already scrolls, so nothing was
 * unreachable, but that guard asserts something stronger and correct — a
 * settings dialog that hides a section below a fold is one nobody discovers.
 *
 * Three columns rather than a taller card or a trimmed section: the comment
 * below already says the point of the multi-column flow is to put more than one
 * topic in view at once, and the 720px cap was the only thing holding it to
 * two. 960px fits 3x17rem plus two gaps inside the padding, still leaves a
 * margin either side of a 1280px laptop, and collapses to two columns and then
 * one on narrower viewports with no breakpoint enumerated — the same mechanism,
 * one number changed. */
/* `--dialog-width-xwide`, not a fourth raw pixel value (HF-265 D8). 960 was
 * already the widest card in the product while `--dialog-width-wide` sat
 * declared and unused — so the scale had a dead rung and this dialog had an
 * unnamed one. Same number, stated in the scale. */
.dialog-card.settings-dialog {
  width: min(var(--dialog-width-xwide), 100%);
  max-inline-size: min(100%, var(--dialog-width-xwide));
}

/* Six topics, read down one column then down the next.
 *
 * Every section here is a heading plus one short control — a three-button
 * segmented, seven 26px swatches, one select — so a single column spent its
 * height on headings and left 250-280px of every row empty beside the control
 * that row exists for. Word's Options answers the same problem with a category
 * rail and Google Docs' Preferences with a much tighter list; both put more than
 * one topic in view at once, which is the part that matters.
 *
 * Multi-column rather than a grid, and it is the mechanism `.shortcuts-body`
 * already uses for the same shape: a grid equalises row heights, so pairing a
 * 54px Accent with a 129px Reviewer identity would pay 75px for the privilege.
 * Columns flow instead, so each section is only as tall as it is.
 *
 * `column-count: 2` is a CEILING, not a target — the used count is the smaller
 * of it and what `column-width` fits, so this is one column on a phone and two
 * in the File page's wide pane, with no breakpoint enumerated. The width is in
 * `rem` and not `vw`: this card is 720px whatever the viewport is, so a
 * viewport-relative track would silently fall back to one column on a large
 * monitor — the opposite of what it reads like. */
.settings-body {
  column-count: 2;
  column-width: 17rem;
  column-gap: clamp(20px, 2.4vw, 32px);
}

/* A setting and its own note must not be split across the column break. */
.settings-section {
  break-inside: avoid;
}

/* 20px between groups, the same figure `.page-setup-fields` and
   `.watermark-body` already use, so a group boundary is one distance in every
   dialog. 14px was close enough to the 8px heading gap that a heading floated
   between the group above it and the group it names. */
.settings-section {
  margin-bottom: var(--space-5);
}

/* Settings had its OWN heading style — 10px, 700, uppercase, `--faint` — while
   every other dialog names a group with `.dialog-group legend` at 13px/650 in
   `--ink`. Two consequences, both visible in a screenshot: Settings did not
   look like Page setup sitting next to it, and inside Settings the group
   heading "REVIEWER IDENTITY" was SMALLER and fainter than the field labels
   "Name" and "Initials" underneath it — the hierarchy inverted. It is the
   shared group heading now; see `.dialog-group legend`. (The uppercase micro
   label survives where it means something different: `.bookmark-list-head` and
   `.shortcuts-group h3` mark sections of a LIST, not groups of a form.) */
.settings-label,
.dialog-group legend,
.metadata-section h3,
.metadata-disclosure summary {
  margin: 0 0 var(--space-2);
  padding: 0;
  color: var(--ink);
  font-size: var(--fs-body);
  font-weight: 650;
  letter-spacing: normal;
  text-transform: none;
}

.segmented {
  display: flex;
  gap: 2px;
  padding: 3px;
  background: var(--bg-2);
  border-radius: 9px;
}

.segmented button {
  flex: 1;
  height: 28px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--muted);
  font-weight: 550;
  cursor: pointer;
}

/* Both spellings: a segmented control is a toggle set (aria-pressed) except
   where the choices are mutually exclusive, which makes it a radio group
   (aria-checked) — the Theme control is the latter, per docs/104 HF-070. */
.segmented button[aria-pressed="true"],
.segmented button[aria-checked="true"] {
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-sm);
}

/* A segmented control laid out as a column, for a radio group whose options are
   sentences rather than words — Restrict Editing's five levels read "No changes
   (read only)" and "Filling in forms", which is Word's own wording and will not
   fit five abreast at any dialog width. Same control, same ARIA, same checked
   treatment; only the axis changes, so nothing about the token system moves. */
.segmented-stacked {
  flex-direction: column;
  align-items: stretch;
}

.segmented-stacked button {
  height: auto;
  min-height: 28px;
  padding: 5px 10px;
  text-align: start;
}

.swatches {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.acc {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 2px solid transparent;
  background: var(--c, var(--accent));
  cursor: pointer;
  padding: 0;
  box-shadow: inset 0 0 0 1.5px var(--surface);
}

.acc[aria-pressed="true"] {
  border-color: var(--ink);
}

.acc-custom {
  display: inline-grid;
  place-items: center;
  background: conic-gradient(red, yellow, lime, cyan, blue, magenta, red);
  overflow: hidden;
}

/* The input FILLS its swatch rather than overhanging it by 14px in each
   direction. It was 40px inside a 26px circle — invisible, because the swatch
   clips, but it is still content painted outside the box that holds it, which
   is what `dialog-contract`'s clipping guard caught the moment Settings became
   a dialog and came under that contract. The hit target is the swatch. */
.acc-custom input[type="color"] {
  width: 100%;
  height: 100%;
  border: none;
  background: none;
  cursor: pointer;
  opacity: 0;
}

.settings-note {
  margin: 8px 0 0;
  color: var(--muted);
  font-size: var(--fs-small);
  line-height: 1.4;
}

/* ---- Modal dialogs --------------------------------------------------------- */
body.modal-open {
  overflow: hidden;
}

.dialog-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  place-items: center;
  padding: var(--space-6);
  /* No `backdrop-filter`. A 2px blur over a full-viewport scrim is re-rasterised
     on every frame that anything above it scrolls, and the 42% scrim is already
     what separates the dialog from the document — the blur was decoration
     bought at the price of a class of scroll jank. */
  background: rgba(20, 22, 28, 0.42);
}

.dialog-overlay[hidden] {
  display: none;
}

.dialog-card {
  display: flex;
  width: min(var(--dialog-width-standard), 100%);
  max-height: min(720px, calc(100vh - 48px));
  flex-direction: column;
  overflow: hidden;
  /* No border. A card that carries BOTH a 1px outline and a 72px drop shadow is
     saying the same thing twice, and the ribbon band next to it already
     established the house rule — "white on grey, so it separates by SHADE and
     needs no border". The dialog sits on a scrim, so the shadow alone lifts it. */
  border-radius: var(--radius-dialog);
  background: var(--surface);
  box-shadow: var(--shadow-dialog);
}

.dialog-card-compact {
  width: min(var(--dialog-width-compact), 100%);
}

/* Two of the compact cards are smaller than the 380px constant and one is
 * bigger, so both are stated per dialog rather than by moving a constant that
 * four dialogs share. See `.field-dialog` for why the spelling is the bare
 * `fit-content` keyword.
 *
 * SPLIT CELL. Word's Split Cells is one of the smallest dialogs it ships — a
 * title, two spinners, OK and Cancel — and ours was a 380px card holding two
 * 144px boxes for a number between 1 and 20, because `.dialog-field-grid`'s
 * shared track floor is 7rem. That floor is right for the margin and paper
 * fields it was written for; it is not right for a cell count, so this dialog
 * states its own. The card then falls to what the ACTION ROW needs, which is
 * the widest thing left in it. */
/* `body:not(.phone-mode)` is load-bearing, not decoration (HF-265 D5).
 * `#splitCellDialog .dialog-card` is specificity (1,1,0) and the phone bottom
 * sheet is `body.phone-mode .dialog-card` at (0,2,0), so an ID written here
 * outranked the rung no matter where it was declared — measured on a 390px
 * phone, Split cell rendered as a 292px slab pinned to the bottom-LEFT of the
 * screen instead of a full-width sheet. `#confirmDialog` below has the same
 * shape and only reached full width when its runtime message happened to be
 * long enough. The desktop qualifier is what makes the two cascades disjoint
 * rather than making the phone rule shout `!important`.
 * `e2e/phone-dialog-sheet.spec.mjs` measures every dialog's left edge. */
body:not(.phone-mode) #splitCellDialog .dialog-card {
  width: fit-content;
  max-inline-size: min(100%, 46ch);
}

#splitCellDialog .dialog-field-grid {
  grid-template-columns: repeat(2, max-content);
}

/* Two digits and a stepper. In `ch` so it follows the font rather than being a
 * pixel that happened to fit Inter on one machine; the clipping contract in
 * `dialog-contract.spec.mjs` is what catches it if a font makes this too tight. */
#splitCellDialog .dialog-field > input {
  width: 11ch;
}

/* CONFIRM. The one card here that was too NARROW, and measurably so: at 380px
 * the footer could not fit "Save the .docx first to keep your work." beside
 * "Keep editing" and "Discard and open", so `.dialog-foot`'s `flex-wrap` put
 * the buttons on their own line and the row stood 86px tall instead of 52px —
 * 34px spent on a wrap nobody asked for — while the message above it broke
 * across four lines. Word and Google Docs both set a confirmation wider than
 * this and keep its buttons on one line.
 *
 * `fit-content` rather than another constant because this dialog's text is
 * supplied at runtime by `confirmModal()`: a short question gets a small card,
 * and the ceiling is the width at which the longest action row this product
 * builds still fits on one line. */
/* Desktop-qualified for the reason `#splitCellDialog` above is. */
body:not(.phone-mode) #confirmDialog .dialog-card {
  width: fit-content;
  max-inline-size: min(100%, 56ch);
}

/* `hidden` is the weakest promise in the platform: it is a UA `display: none`,
 * and ANY author `display` beats the entire UA sheet. So a rule as ordinary as
 * `display: flex` un-hides every element it matches, silently — the markup
 * still says `hidden` and the JS still sets it.
 *
 * Two of these were live in these dialogs, both found by looking rather than by
 * reading. `.link-mode-panel { display: flex }` was painting the INACTIVE tab
 * panel of the link dialog: with "Web address" chosen, the dialog also showed
 * the bookmark picker reading "No bookmarks in this document" and its note —
 * 52px of a 509px dialog that the user had not asked for, which also made the
 * two tabs look inert. `.bookmark-list { display: flex }` was painting an empty
 * hidden list at 416x0, whose only effect was to spend one 16px flex gap in the
 * middle of the "No bookmarks yet" empty state.
 *
 * The stylesheet answers this per component in about a dozen places
 * (`.ribbon-panel[hidden]`, `.side-panel[hidden]`, `.context-menu[hidden]`, …),
 * and every one of those is a rule somebody had to remember. Inside a dialog it
 * is answered once. `!important` is the point rather than a shortcut: the rule
 * has to outrank an author `display` it has never seen, which is the only thing
 * that can go wrong here. Guarded by `dialog-contract.spec.mjs` — "nothing it
 * has hidden is painted", over every modal. */
.dialog-card [hidden] {
  display: none !important;
}

/* No rule under the head, and none over the foot — see `.dialog-body` below for
   what replaces them. A dialog was three stacked bands: a white head, a
   hairline, a white body, a hairline, and a GREY footer. Three bands is a box
   drawn twice inside a box, and it is the largest remaining reason these read
   as heavier than the rest of the app: the footer alone is a 49px full-width
   slab in a different colour from everything above it. Neither Word's dialogs
   nor Google Docs' divide a dialog this way — the card is one surface, and the
   action row sits on it. */
.dialog-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--dlg-pad-y) var(--dlg-pad-x);
}

.dialog-heading {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: var(--space-3);
}

.dialog-heading-icon {
  display: grid;
  width: 36px;
  height: 36px;
  flex: 0 0 36px;
  place-items: center;
  border-radius: var(--radius-md);
  background: var(--accent-soft);
  color: var(--accent-text);
  font-size: var(--icon-rail);
}

/* Chrome text interpolates user data — filenames, style names, search terms —
 * and a filename is routinely one unbreakable token: CSS does not break on an
 * underscore, so `General_Loan_On_lend_and_loan_from_SMSF_Agreement.docx` is a
 * single 48-character word. A `fit-content` card cannot shrink below it, and
 * the text was painted past the card's right edge and clipped mid-word. The
 * measured guard missed it because its fixture was `opendoc-demo.docx`.
 *
 * `anywhere` rather than `break-word`: `break-word` still lets the unbreakable
 * token set the element's min-content width, so a `fit-content` ancestor keeps
 * sizing to it and the overflow comes back. `anywhere` lets the break count
 * toward min-content, which is the whole point here. Applied at the text
 * elements rather than the card so a deliberately non-wrapping control —
 * a button label — is unaffected. */
.dialog-heading h2,
.dialog-heading p,
.dialog-body,
.dialog-note {
  overflow-wrap: anywhere;
}

.dialog-heading h2 {
  margin: 0;
  color: var(--ink);
  /* One step down from `--fs-title`. A dialog title names a task in a box the
     user already chose to open; it is not a page heading and does not need to
     compete with the document. Word and Docs both set theirs at body weight
     plus a little. */
  font-size: var(--fs-body);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.dialog-heading p {
  margin: 2px 0 0;
  color: var(--muted);
  font-size: var(--fs-small);
}

/* A confirmation has no `.dialog-body`: its whole message IS this paragraph, so
   on those cards it is the content and not a subtitle. It was being painted at
   12px in `--muted` — "Opening another file replaces X. Your edits have not been
   saved and there is no copy of them anywhere else." set smaller and quieter
   than the title above it, which is the one sentence in the product a user most
   needs to read. Word and Docs both set a confirmation's message at body size
   in body colour. Structural rather than per-dialog: any card that carries its
   message in the head gets this, and any card with a body is unaffected. */
.dialog-card:not(:has(.dialog-body)) .dialog-heading p {
  margin-top: var(--space-2);
  color: var(--ink);
  font-size: var(--fs-body);
  line-height: 1.45;
}

.dialog-close {
  display: grid;
  width: var(--h-control);
  height: var(--h-control);
  flex: 0 0 var(--h-control);
  place-items: center;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.dialog-close .ms {
  font-size: var(--icon-control);
}

.dialog-close:hover {
  background: var(--bg-2);
  color: var(--ink);
}

/* Scroll shadows, the established answer to "where did the divider go".
 *
 * The two hairlines the head and foot used to carry were painted whether or not
 * there was anything on the other side of them, so on About — six lines of
 * content — they drew a boundary around nothing. What a divider is actually for
 * is saying "there is more content above/below the fold", and that is a state,
 * not a constant. The technique is Roman Komarov's `background-attachment:
 * local` pair: two cover gradients painted in the SCROLLER's coordinates hide
 * two shade gradients painted in the VIEWPORT's, so each shade is revealed
 * exactly when that edge has content scrolled past it. No JS, no scroll
 * listener, and it is correct on the first frame.
 *
 * `color-mix` rather than a new token: the shade is --ink at 11%, so it follows
 * the theme by construction and adds no colour literal to feature CSS. */
.dialog-body {
  min-height: 0;
  overflow: auto;
  padding: var(--dlg-pad-y) var(--dlg-pad-x);
  overscroll-behavior: contain;
  background:
    linear-gradient(var(--surface) 30%, transparent) top / 100% 18px no-repeat local,
    linear-gradient(transparent, var(--surface) 70%) bottom / 100% 18px no-repeat local,
    linear-gradient(color-mix(in srgb, var(--ink) 11%, transparent), transparent) top / 100% 7px
      no-repeat scroll,
    linear-gradient(transparent, color-mix(in srgb, var(--ink) 11%, transparent)) bottom / 100% 7px
      no-repeat scroll,
    var(--surface);
}

/* Keyboard shortcuts reference (HF-151). Two columns on a wide card so the
 * whole keymap is scannable without scrolling past the group you want; one
 * column as soon as that would squeeze the key chips.
 *
 * Measured at 1280x720: the card took the 760px "wide" constant, which gave the
 * body 716px and two multicol columns of 346px. The widest GROUP in the keymap
 * — a group is only as wide as its widest label plus its widest chord — is
 * 250px (REVIEW), and five of the eight are under 200px. So 96px of every
 * column, 192px of the card, was empty gutter: a constant, not a measure.
 *
 * The width is in `ch` rather than pixels because what it has to hold is TEXT.
 * The groups grow with the font and so does this, which a pixel tuned on one
 * machine's Inter would not — and the Linux runner's fonts are not these.
 * 80ch leaves the two columns about 36px of slack over the widest group here,
 * and `.shortcuts-group` below now wraps its label column rather than
 * overflowing if a translation or a substituted font eats that slack. */
/* 80ch was the wrong UNIT for this card, and that is why it overflowed
 * (HF-265 D3). Measured at 1280x720 before this change: 80ch resolved to about
 * 560px, which left the body 516px of inner width — and `.shortcuts-body`'s
 * multicol track is `clamp(15rem, 22vw, 17rem)`, i.e. 272px at this viewport,
 * so only ONE column fitted. One column of the whole keymap is 872px of
 * content in a 612px box: the reference ran off the bottom of a card that had
 * no footer under it either.
 *
 * The card's width here is not set by how wide its TEXT wants to be — it is set
 * by how many `rem`-sized multicol tracks have to fit, so it is stated in the
 * same pixel scale the tracks are: `--dialog-width-xwide` (960px) holds three
 * 272px tracks plus two gaps inside the dialog padding and still leaves a
 * margin either side of a 1280px laptop. Below that the track count falls to
 * two and then one with no breakpoint enumerated, exactly as before. Keyboard
 * shortcuts is in `dialog-fit`'s roster now — with all 23 of them — so neither
 * the overflow nor the single column can come back unnoticed. */
.shortcuts-dialog {
  width: min(var(--dialog-width-xwide), 100%);
}

/* About is a short fact list, so it is sized by its CONTENT rather than by a
   dialog-width constant — the habit that makes a four-field dialog as wide as
   a twenty-field one and leaves the middle empty. `fit-content` with a ceiling
   keeps it honest at every viewport, including 390px. */
.about-dialog {
  /* `fit-content`, NOT `min(fit-content, 100%)` — see `.field-dialog`. */
  width: fit-content;
  /* Three facts and three links. A `ch` ceiling keeps the description from
     dragging the card out to the 640px standard constant, which is nearly
     three times what the content needs. */
  max-inline-size: min(100%, 46ch);
}

/* Two columns: term and value on one line, the way a properties sheet reads.
   `auto auto` so neither column is wider than the text it holds. */
/* The brand mark in place of a generic info glyph: About is the one dialog
   whose subject IS the product. */
.about-mark {
  flex: none;
  width: 36px;
  height: 36px;
}

.about-facts {
  display: grid;
  grid-template-columns: auto auto;
  gap: var(--space-2) var(--space-5);
  margin: 0;
  align-items: baseline;
}

.about-facts dt {
  color: var(--ink-2, var(--ink));
  font-size: var(--fs-caption, 12px);
}

.about-facts dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}

.about-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin: var(--space-5) 0 0;
}

.about-links a {
  color: var(--accent-text);
}

/* The deployment's own identity (ONLYOFFICE `customization.customer`), rendered
   by `about_dialog.mjs` only when `brand.json` configures one — so the shipped
   build pays nothing for it. Separated from the product's own facts by a rule,
   because "who wrote this editor" and "who runs this deployment" are two
   different answers and running them together would read as one. */
.about-customer {
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--line);
}

/* One element in both themes, painted from a token the renderer sets inline.
   The dark rule falls back to the light value, so a host who named only `logo`
   is correct everywhere and a host who named both is correct in all three theme
   entry points — the third being a reader who chose Dark on a light OS, which is
   the one `docs/104` HF-092 was shipped without. */
.about-customer-mark {
  display: block;
  width: 28px;
  height: 28px;
  margin-bottom: var(--space-2);
  background: var(--about-customer-mark, none) center / contain no-repeat;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) .about-customer-mark {
    background-image: var(--about-customer-mark-dark, var(--about-customer-mark, none));
  }
}

:root[data-theme="dark"] .about-customer-mark {
  background-image: var(--about-customer-mark-dark, var(--about-customer-mark, none));
}

.about-customer-name {
  margin: 0;
  font-weight: 600;
}

.about-customer-info,
.about-customer-line {
  margin: var(--space-1) 0 0;
  color: var(--muted);
}

.about-customer .about-links {
  margin-top: var(--space-4);
}

.shortcuts-body {
  /* Column COUNT derives from the width available, rather than being fixed at
     two and overridden at one breakpoint: four fit on the 960px card, two at a
     620px window, one at 390, and nothing in between has to be enumerated.
   *
     The track was `clamp(15rem, 22vw, 17rem)`, and the `22vw` term is why the
     clamp never did what it looks like it does: at 1280 it resolves to 281.6px
     and is pulled back to the 17rem ceiling, so the floor was unreachable and
     the track was a constant 272px at every desktop width. Three such tracks
     put 643px of keymap in a 560px body on a 1280x720 laptop (measured), which
     is the overflow `dialog-fit.spec.mjs` now covers for this dialog.
   *
     A flat 13rem instead. The label column is `minmax(0, max-content)`, so a
     label that does not fit a track WRAPS inside it and cannot paint across the
     next column — which is what makes a narrower track safe here, and it is
     already the reason that spelling was chosen.

     MEASURED AFTER, and the two steps reported separately because they are not
     the same size: four columns of 211px take the eleven groups from 643px to
     540px, and the group margin below takes 540 to 532, in a body whose ceiling
     on this viewport is 560.

     THAT 28px OF HEADROOM WAS NOT REAL, and the argument that made it look safe
     was wrong. It ran: this card's type is SELF-HOSTED, so the Linux runner lays
     these rows out in the same faces a Mac does. The faces are the same; the
     METRICS are not. CI measured the same body at 577px against the same 560px
     ceiling — `#shortcutsDialog hides 17px of its content below the fold` — a
     45px platform delta on a 28px margin. Same font file is not same line box.

     So `.shortcuts-group` lets a group break across columns, which is the 57px
     the previous pass measured and declined. Four configurations, all measured
     at 1280x720 through `modal-roster`'s own open path, with the CI delta
     applied:

       break-inside | track | local | projected CI (+45px)
       avoid        | 13rem |  532  |  577  <- what CI rejected
       avoid        | 10rem |  517  |  562  <- still over the 560 ceiling
       auto         | 13rem |  475  |  520  <- taken
       auto         | 10rem |  453  |  498

     A narrower track alone cannot fix this: `break-inside: avoid` means no
     column is shorter than the tallest group and Paragraph alone is 281px, so
     10rem buys 15px where 45px is needed. The track stays at 13rem rather than
     going to 10rem, because narrowing it is a SECOND readability cost on top of
     the split and 520px already clears the ceiling by more than the delta that
     produced this failure.

     The cost is real and is the one the previous pass named: a keymap is read by
     scanning a group, and a group split down a column boundary is the thing
     multicol is normally asked not to do. It is accepted because the
     alternative on a 1280x720 laptop is content below the fold, and a group a
     reader cannot see at all is worse than one that continues in the next
     column. Multicol preserves reading order, so a split group continues where
     a reader's eye is already going. */
  columns: auto;
  column-width: 13rem;
  column-gap: var(--space-6);
}

/* The GROUP owns the two columns, not each row.
 *
 * Every row was its own `max-content max-content` grid, so each chord started
 * wherever its own label happened to end: inside MOVING AROUND the keys began
 * at seven different x positions in seven consecutive rows, and a keymap is
 * read by scanning the key column. Aligning per ROW is what the previous pass
 * fixed away from — it had been `space-between` across a 347px column, leaving
 * a 235px median gap. Per GROUP is the third option and the one both reference
 * products use: the pair still sits together, because the group is only as wide
 * as its widest label plus its widest chord, and the column is straight.
 *
 * `display: contents` on the row hands its two cells to this grid; the row
 * padding it drops comes back as `row-gap`. */
.shortcuts-group {
  break-inside: auto;
  display: grid;
  /* `minmax(0, max-content)` on the LABEL column, not bare `max-content`: the
     card is now sized to the measured width of these groups, so a group that
     grew — a longer translation, a substituted font on another platform — has
     to wrap inside its column instead of painting across the next one. The
     chord column stays `max-content`: a key chord must never break. */
  grid-template-columns: minmax(0, max-content) max-content;
  justify-content: start;
  align-items: baseline;
  column-gap: clamp(8px, 1vw, 16px);
  row-gap: 6px;
  /* One step down from `--space-5` (HF-265 D3). Eleven groups paid that margin
     eleven times — 220px spread over four columns — and the uppercase group
     heading is what separates the groups anyway, as it always was. Measured:
     540px of content to 532px, in a 560px box. A small step, reported as one:
     the track correction above is what did the work, this is the last 8px. */
  margin: 0 0 var(--space-4);
}

.shortcuts-group .shortcuts-row {
  display: contents;
}

/* Still the uppercase micro label rather than the form group heading: this
   marks a section of a reference LIST, which is a different job from naming a
   group of controls. Same reason `.bookmark-list-head` keeps it. */
.shortcuts-group h3 {
  grid-column: 1 / -1;
  margin: 0 0 var(--space-2);
  font-size: var(--fs-small);
  font-weight: 650;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--faint);
}

.shortcuts-row {
  /* `space-between` pushed the key chip to the far edge of a fixed 347px
     column, so the gap between a label's last glyph and its chord measured a
     median of 235px — 68% of every row was empty. The pair now sits together
     and the row is as wide as it needs to be. */
  display: grid;
  grid-template-columns: max-content max-content;
  justify-content: start;
  align-items: baseline;
  column-gap: clamp(8px, 1vw, 16px);
  padding: 3px 0;
}

.shortcuts-row span:first-child {
  color: var(--ink);
}

.shortcuts-keys {
  flex: 0 0 auto;
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  white-space: nowrap;
}

.dialog-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* The action row WRAPS rather than clipping. With `nowrap` the note and the
     two buttons were squeezed into one line of a 380px compact card and the
     labels broke inside boxes that could not grow (see `.dialog-button`). */
  flex-wrap: wrap;
  gap: var(--space-4);
  /* The action row needs less vertical air than the body: it holds one line of
     buttons. On the compact dialogs head+foot were eating 55% of the card.
     No fill and no rule (see `.dialog-head`): the buttons sit on the card, as
     they do in Word and in Google Docs. The extra bottom padding is what now
     separates them from the edge — it was the grey band doing that before. */
  padding: calc(var(--dlg-pad-y) * 0.8) var(--dlg-pad-x) var(--dlg-pad-y);
}

.dialog-note {
  /* Takes the leftover width, but gives the buttons their line when what is
     left is too narrow to read: the note is the least important thing in the
     footer, and it must never be what forces a label to break. */
  flex: 1 1 16ch;
  min-width: 0;
  color: var(--muted);
  font-size: var(--fs-small);
}

.dialog-actions {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-left: auto;
}

.dialog-button {
  /* `min-height`, not `height`. A fixed height is a promise that every label
     fits on one line, and the app writes its own labels at runtime:
     `confirmModal({ confirmLabel: "Discard and open" })` wrapped to two lines
     and reported a 59px content height inside this 32px box, so the second
     line was simply not painted. `nowrap` keeps the normal case on one line —
     the rendering is unchanged for every label that already fitted — and
     `min-height` lets the box grow instead of cutting anything that does not. */
  min-height: var(--h-search);
  flex: 0 0 auto;
  white-space: nowrap;
  min-width: 80px;
  padding: 0 16px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font-weight: 600;
  cursor: pointer;
}

.dialog-button:hover:not(:disabled) {
  border-color: var(--muted);
  background: var(--bg-2);
}

/* A disabled dialog button has to LOOK disabled.
 *
 * `.dialog-button` had no `:disabled` rule and its `:hover` applied regardless,
 * so a button that ships disabled-with-a-reason rendered identically to a live
 * one — same ink, `opacity: 1`, `cursor: pointer` — and lit up on hover. Found
 * by screenshotting the version-history panel: "Show changes" is genuinely
 * `disabled` and carries the right title ("Comparing one version with another
 * is not built yet"), and it was indistinguishable from "Restore this version"
 * beside it. `SKILL.md` §10's rule is that a command which cannot run ships
 * disabled WITH A REASON rather than as a button that does nothing — a reason
 * only a tooltip can reveal, on a control that looks live, is the second half of
 * that rule unmet.
 *
 * The values are the ones the rest of the chrome already uses (`.btn:disabled`,
 * `.save-format:disabled`), not new ones: one treatment for one meaning. */
.dialog-button:disabled {
  opacity: 0.45;
  cursor: default;
}

.dialog-button-primary {
  border-color: var(--primary-bg);
  background: var(--primary-bg);
  color: var(--primary-ink);
}

/* `:not(:disabled)` is load-bearing, not symmetry with the rule above.
   Without it this selector is (0,2,0) and `.dialog-button:hover:not(:disabled)`
   is (0,3,0), so the GENERIC hover won on a primary button and repainted it
   `--bg-2` while leaving the white primary text in place: measured
   rgb(251,250,248) on rgb(244,246,251), about 1.03:1, in every dialog in the
   product. It was invisible to the contrast sweep because the sweep reads
   resting state, and invisible to every behavioural test because the button
   still worked — it was found by hovering one and looking. `docs/63`'s AA floor
   is 4.5:1 for text. */
.dialog-button-primary:hover:not(:disabled) {
  border-color: var(--primary-bg-hover);
  background: var(--primary-bg-hover);
}

/* ---- Document properties --------------------------------------------------- */
/* Two columns, because the dialog holds two topics.
 *
 * Measured at 1280x720, the laptop where the complaint shows: the card was
 * 640x672 — the full height the shell allows — and the body still reported 696px
 * of content in 560px of box. 136px of the dialog was below the fold, and what
 * was below it was the file's own facts: Revision, Language, Content status,
 * Version, and the whole "Producer and saved statistics" disclosure.
 *
 * The cause is that one column was carrying two unrelated things: six fields you
 * TYPE, then eight facts you READ. Word does not stack those either — its
 * Properties dialog puts them on separate tabs (Summary, Statistics). Tabs need a
 * script and a second dismissal surface, so the same separation is spatial here:
 * the form on the left, the facts on the right, both visible at once, which is
 * strictly more than a tab strip gives.
 *
 * `auto-fit` rather than two fixed tracks so the phone case needs no breakpoint:
 * below ~570px of body only one 268px track fits and the two halves stack, which
 * is the layout this replaces and the right one at that width. */
.properties-dialog {
  /* Unchanged at the 640px standard constant. The fix here is the SHAPE, not
     the width: two 268px tracks and the gap floor need 556px and the body has
     596px, so the split costs nothing on the horizontal and a wider card would
     only take more of the screen for the same content. */
  width: min(var(--dialog-width-standard), 100%);
}

.properties-body {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 268px), 1fr));
  /* No ROW gap. There is only ever one row here, except when the two halves
     stack — and there the facts block brings its own 20px margin and 16px
     inset back with the separator, so a row gap on top of that would spend an
     extra 20px on the narrowest screen, which is the one with none to spare. */
  gap: 0 clamp(20px, 2.4vw, 32px);
  align-content: start;
  /* So the rule below can ask whether the two halves are actually side by side
     — see the `@container` query. A grid stretched by its parent's width does
     not size to its contents, which is the condition `inline-size` containment
     requires. */
  container-type: inline-size;
}

.properties-facts {
  min-width: 0;
}

/* The rule and the 36px of air above "File details" separated it from the form
   when the form was directly ABOVE it. Beside the form, the same rule separates
   it from the top of the card instead, and the column edge already says what
   the hairline was saying — so it goes.
   It comes back when the two halves stack, which is a fact about the container
   and not about the viewport: the same body is wide in a dialog, wide in the
   File page's pane, and narrow on a phone. 555px is one pixel under the two
   268px tracks plus the 20px gap floor, i.e. exactly the width at which
   `auto-fit` gives up on the second column — so the two rules can never
   disagree about which layout is on screen. The disclosures under it keep their
   separator in both, because there the boundary is still between two stacked
   things. */
.properties-facts > .metadata-section:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

@container (max-width: 555px) {
  .properties-facts > .metadata-section:first-child {
    margin-top: var(--space-5);
    padding-top: var(--space-4);
    border-top: 1px solid var(--line);
  }
}

/* 16px between rows against the 6px that binds a label to its own input: a
   label must be visibly closer to the control it names than to the one above
   it, and at 14px against 6px that ratio was 2.3 — near enough to ambiguous
   that the eye had to read the words to pair them. Word and Docs both run
   about 3:1 here. */
.properties-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4) var(--space-5);
}

.field-wide {
  grid-column: 1 / -1;
}

.dialog-field {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 6px;
  color: var(--muted);
  font-size: var(--fs-small);
  font-weight: 600;
}

.dialog-field > input,
.dialog-field > textarea {
  width: 100%;
  min-width: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-body);
  font-weight: 450;
}

.dialog-field > input {
  height: var(--h-field);
  padding: 0 10px;
}

.dialog-field > textarea {
  min-height: 76px;
  padding: 9px 10px;
  line-height: 1.45;
  resize: vertical;
}

.dialog-field > input:focus,
.dialog-field > textarea:focus,
.number-control:focus-within {
  outline: none;
  border-color: var(--accent-text);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.metadata-section,
.metadata-disclosure {
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--line);
}

/* Typography lives with the shared group heading (`.settings-label` block). */
.metadata-disclosure summary {
  cursor: pointer;
}

.metadata-facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px 18px;
  margin: 0;
}

/* Rows, not cards. These eight facts are READ-ONLY metadata; boxing each one in
   a filled, outlined card gave the dialog eight more rectangles than it has
   pieces of information, which is most of what made it read as boxy. Word's
   Info pane and Docs' Document details both list them as plain label/value
   pairs with a hairline between. The row separator carries the structure the
   box used to, at a fraction of the weight. */
.metadata-facts > div {
  min-width: 0;
  padding: 7px 0;
  border-bottom: 1px solid var(--line);
}

.metadata-facts dt {
  margin-bottom: 3px;
  color: var(--muted);
  font-size: var(--fs-caption);
  font-weight: 600;
}

.metadata-facts dd {
  margin: 0;
  overflow: hidden;
  color: var(--ink);
  font-size: var(--fs-small);
  font-weight: 550;
  line-height: 1.35;
  text-overflow: ellipsis;
}

.metadata-facts dd.metadata-empty {
  color: var(--faint);
  font-weight: 450;
}

.metadata-disclosure:not([open]) summary {
  margin-bottom: 0;
}

.metadata-facts-compact {
  gap: 8px 12px;
}

.metadata-fact-wide {
  grid-column: 1 / -1;
}

/* ---- Page setup dialog ----------------------------------------------------- */
.page-setup-dialog {
  /* Driven by what it holds, not by the "wide" constant. With the preview
     sized to its thumbnail and the spinners sized to five characters, the
     content no longer fills 760px, and a dialog that stays wide after its
     contents shrink is exactly the "bigger but not more useful" complaint.
     The ch ceiling keeps a long margin label from stretching it back out. */
  /* This declaration DOES NOT PARSE and never has — see `.field-dialog`, which
     explains why, and `dialog_density.test.mjs`, which ratchets it. Left as
     written rather than corrected in passing: `fit-content` on its own would size
     this card to its content, and what this dialog needs is the opposite — a
     stated width wide enough to hold two columns of groups. The declaration below
     is the one that applies. */
  width: min(fit-content, 100%);
  /* A STATED width. MEASURED, on a 1280x720 laptop: the body held 602px of
     content in a 560px box, so 42px of this dialog — the bottom of the Columns
     group — was already below the fold before this round added anything, and
     `dialog-fit.spec.mjs` did not cover it. Two more groups (a Gutter field, which
     `auto-fit` absorbs into the existing Margins rows for free, and Vertical
     alignment, which does not) would have made that 108px. The fix is two balanced
     columns of groups, which needs a width the single tall stack did not.

     756px AND NOT WIDER, and the ceiling is not arbitrary: `--dialog-width-wide` is
     760px, the widest constant in this file's dialog scale, and
     `header-menu.spec.mjs` asserts this card stays under it. A first attempt at
     this layout used 900px, which made Page setup wider than any token in the
     design system — that is a change to the scale, and it belongs to the owner
     rather than to a section-properties round. It reddened `header-menu` (received
     900, expected < 760), and that is why the two declarations below exist.

     What 756px costs, and how it is paid — every figure measured, not estimated:
     at 756 the two columns are 265px each, and `.dialog-field-grid`'s shared track
     floor is 144px at this viewport, so each column held ONE field and the five
     margin fields stacked into 701px of content in a 560px box — WORSE than the 602
     it started at. So the preview column gives back 32px it was not using, and the
     field floor is lowered FOR THIS DIALOG ONLY to 7.75rem, which puts two fields
     back in a column. Result: 485px of content in 485px of box, card 596px tall.
     Page setup is in `dialog-fit`'s roster now, so neither the overflow nor the
     width can come back unnoticed. */
  width: min(756px, 100%);
  /* The stated ceiling, from the scale rather than from a `ch` count that
     happened to land near it: `--dialog-width-wide` IS the 760px this comment
     already names, and `header-menu.spec.mjs` asserts this card stays under it.
     It does not bind (the width above caps at 756), so nothing renders
     differently — but the token is now read where the prose already relied on
     it, which is the point of a scale (HF-265 D8). */
  max-inline-size: min(100%, var(--dialog-width-wide));
}

/* `min(fit-content, 100%)` IS NOT VALID CSS, and it is written on five cards in
 * this file — every one that was meant to be sized by its contents. `min()`
 * takes <length-percentage> terms and `fit-content` is a sizing KEYWORD, so the
 * whole declaration is dropped at parse time: `CSS.supports("width",
 * "min(fit-content, 100%)")` is `false` in Chromium, and the computed width of
 * every one of those cards was still `.dialog-card`'s own `min(640px, 100%)`.
 *
 * So what they actually had was the 640px constant clamped by their
 * `max-inline-size` ch ceiling — a constant, which is the very thing they were
 * written to stop being. Insert field measured 426px, which is exactly its
 * 52ch ceiling and not the 357px its rows add up to.
 *
 * `fit-content` on its own is the correct spelling and needs no clamp: it is
 * defined as `min(max-content, max(min-content, stretch))`, so it already
 * cannot exceed the space available to it. `dialog_density.test.mjs` requires
 * the keyword form on the cards it names and ratchets the broken one, because
 * the broken spelling is the obvious way to write this and reads as correct.
 *
 * Insert field is a six-row picker whose widest row measures 313px; at the
 * inherited constant, 69px of the card and 69px of every row were empty. */
.field-dialog {
  width: fit-content;
  max-inline-size: min(100%, 52ch);
}

.page-setup-body {
  display: grid;
  /* The preview column was a hard 250px and stretched to the full row height —
     587px of box around a 150x212 sheet, about a fifth of the whole dialog.
     It is now sized by the thumbnail and pinned to the top, as Word's is.
     Narrowed again from `clamp(120px, 13vw, 160px)`: the sheet inside it is
     `min(150px, 100%)` and a laptop resolved that clamp to its 160px cap, so 32px
     of the column was empty. Those 32px are what let the field grid below keep two
     fields per column inside a card that stays under the 760px wide constant — see
     `.page-setup-dialog`. */
  grid-template-columns: clamp(110px, 10vw, 128px) minmax(0, 1fr);
  gap: clamp(16px, 2vw, 28px);
  align-items: start;
}

/* No frame. The preview was a 190x288 filled, outlined, 22px-padded card around
   a 150x212 picture of a sheet — a box drawn round a box drawn round a box, and
   the single biggest rectangle left in any of these dialogs. Word's page-setup
   preview is the sheet alone on the dialog's own surface, and the sheet already
   carries a border and a shadow that say "this is paper". Dropping the frame
   also gives the field column back ~50px, which is where the dialog was short. */
.page-preview {
  display: flex;
  align-self: start;
  block-size: fit-content;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: var(--space-3);
  padding: 2px 0 0;
}

.page-preview-sheet {
  position: relative;
  /* `min()`, not a constant: landscape is 210px and the preview column clamps
     to as little as 120px, so a fixed width painted the sheet outside the
     column that holds it. The portrait default fits today, which is the only
     reason the clipping guard has not seen it — the guard opens the dialog in
     its default state and landscape is one click away. */
  width: min(150px, 100%);
  aspect-ratio: var(--page-ratio, 8.27 / 11.69);
  overflow: hidden;
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  background: var(--paper);
  box-shadow: 0 4px 14px rgba(20, 22, 28, 0.13);
  transition: width 0.16s ease, aspect-ratio 0.16s ease;
}

.page-preview-sheet[data-orientation="landscape"] {
  width: min(210px, 100%);
}

.page-preview-margins {
  position: absolute;
  inset:
    var(--preview-margin-top, 8%)
    var(--preview-margin-right, 8%)
    var(--preview-margin-bottom, 8%)
    var(--preview-margin-left, 8%);
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding-top: 4px;
  border: 1px dashed var(--line-strong);
}

.page-preview-margins span {
  display: block;
  height: 2px;
  margin-inline: 10%;
  border-radius: 999px;
  background: var(--bg-2);
}

.page-preview-margins span:nth-child(2) {
  width: 68%;
}

.page-preview-margins span:nth-child(3) {
  width: 82%;
}

.page-preview-margins span:nth-child(4) {
  width: 54%;
}

.page-preview-label {
  color: var(--muted);
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
}

/* Balance independent groups across three columns on a laptop. With the
   presets and document controls, two columns held 712px of content in a 560px
   body at 1280x720. Three columns fit the same controls in 530px without
   reducing their font or touch size. Keep each group whole and preserve DOM
   tab order; the narrow-screen rule below returns to one column. */
.page-setup-fields {
  min-width: 0;
  columns: 3;
  column-gap: clamp(16px, 2vw, 20px);
}

/* Text labels carry orientation without the decorative icons in these narrow
   tracks. Action labels may wrap; their buttons grow with the text. */
.page-setup-fields .orientation-segmented {
  height: auto;
}
.page-setup-fields .orientation-segmented.segmented button {
  height: auto;
  min-height: 28px;
  padding-inline: 6px;
  white-space: nowrap;
}
.page-setup-fields .orientation-segmented .ms {
  display: none;
}
.page-setup-fields .chip-btn {
  height: auto;
  min-height: 24px;
  white-space: normal;
}

/* The rhythm is a bottom margin rather than the flex `gap` this was, because a
   gap does not exist between multi-column boxes. `.dialog-group` zeroes its own
   margin, so this is scoped to the children of this one container. */
.page-setup-fields > .dialog-group {
  margin-bottom: 20px;
  break-inside: avoid;
}

/* A LOWER track floor than the shared one, for this dialog only.
   `.dialog-field-grid`'s floor is `clamp(7rem, 20vw, 9rem)`, and `20vw` is the
   VIEWPORT's width, not the column's — so on any laptop it resolves to the 9rem
   cap, 144px. That floor was written for fields laid out across the full width of a
   dialog body. Here they sit inside a 265px multi-column column, where 144px means
   exactly one field per row and five margin fields stacking into a 701px body.
   7.75rem (124px) fits two, which is what the Top/Bottom and Left/Right pairs are
   supposed to be — `.dialog-field-grid`'s own comment refuses stacking them, and at
   this column width this is the only way to honour that. Scoped, because the shared
   floor is right everywhere else it applies. */
.page-setup-fields .dialog-field-grid {
  grid-template-columns: repeat(auto-fit, minmax(clamp(7rem, 20vw, 7.75rem), max-content));
}

/* ---- Caption and Cross-reference (`109` OO-005) ---------------------------- */
/* These two dialogs shipped with NO CSS AT ALL. Every class their markup declared
   — `.caption-body`, `.caption-label-actions`, `.caption-preview-row`,
   `.crossref-targets`, `.crossref-list` and both card classes — had zero rules in
   this file, so they fell back to default block layout: a 596px-wide text input
   beside 98px selects, the two label buttons orphaned on a row of their own, and
   the cross-reference target LIST — the thing you have to pick from — 55px tall
   inside a 289px card. Measured before writing any of this.

   The arrangement follows ONLYOFFICE, whose `CaptionDialog.js:61-120` is a
   four-column table: caption text with Position beside it, then the Label combo
   with its Add and Delete buttons ON THE SAME ROW bottom-aligned, then exclude,
   numbering, the chapter pair, and the example last. Ours stacked all of that
   full-width, which is what made it read as a wall. */

.caption-body,
.crossref-body {
  /* The shared `.dialog-body` already owns padding and scrolling; this only sets
     the rhythm between groups, which is what was missing. */
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Caption text takes the room it needs; Position is a two-option select and does
   not (`CaptionDialog.js:69-71` gives it 75px beside a full-width caption box). */
.caption-text-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) max-content;
  gap: var(--space-3);
  align-items: end;
}

/* Label + its two actions on ONE row, bottom-aligned, as theirs are
   (`CaptionDialog.js:75-85`). They were a 596x34 strip of their own. */
.caption-label-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) max-content max-content;
  gap: var(--space-2);
  align-items: end;
}

/* A pair of selects reads as a pair when the columns match. `.dialog-field-grid`
   sizes cells by content on purpose — right for a Top/Bottom number pair, wrong
   here, where it gave 98px beside 170px and 218px beside 130px. */
.caption-pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3);
  align-items: end;
}

/* Numbering is one narrow select. Left it in the flow rather than stretching it
   across the card, which is where 490px of the emptiness came from. */
.caption-numbering {
  max-inline-size: 16ch;
}

/* A grid cell only equalises the CELL. `.dialog-select` sizes to its longest
   option, so the pairs still came out 218px beside 130px and 106px beside 231px
   until the control was told to fill the column it was given. `min-inline-size: 0`
   because a select's intrinsic minimum otherwise refuses to shrink and pushes the
   grid wider than the card. */
.caption-text-row .dialog-field,
.caption-label-row .dialog-field,
.caption-pair .dialog-field {
  min-inline-size: 0;
}

.caption-text-row > .dialog-field > input,
.caption-label-row > .dialog-field > .dialog-select,
.caption-pair > .dialog-field > .dialog-select {
  inline-size: 100%;
  min-inline-size: 0;
}

.caption-preview-row {
  display: flex;
  gap: var(--space-2);
  align-items: baseline;
  margin: 0;
  padding-top: var(--space-2);
  border-top: 1px solid var(--line);
}

.caption-preview-label {
  color: var(--muted);
  font-size: var(--fs-caption);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.caption-preview {
  font-weight: 600;
}

/* The target list is the CONTENT of the cross-reference dialog, not a footnote to
   it. ONLYOFFICE gives theirs 368x161 (`CrossReferenceDialog.js:91`); ours had 55
   and the reader had to scroll a five-item list. A min-height rather than a fixed
   one so a short document does not leave a hole. */
.crossref-targets {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-height: 0;
}

.crossref-list {
  min-height: 9.5rem;
  max-height: 16rem;
}

/* ---- Watermark (`109` OO-006) --------------------------------------------- */
/* Sized by what it holds, like `.page-setup-dialog` above: the widest row is
   the three-cell Font/Size/Color grid, so the 640px standard constant would
   leave a third of every row empty. */
.watermark-dialog {
  /* `fit-content` on its own, NOT `min(fit-content, 100%)` — see `.field-dialog`
     for why that spelling never parsed. `fit-content` already cannot exceed the
     available space, so the `min()` bought nothing even in intent. */
  width: fit-content;
  max-inline-size: min(100%, 56ch);
}

/* ---- Header and footer settings ------------------------------------------- */
/* Sized by what it holds, like the cards above: the widest row is the Position
   pair, and the standard 640px constant would leave a third of every row empty.
   `fit-content` on its own, NOT the unparseable `min(fit-content, …)` — see
   `.field-dialog`. */
.header-footer-settings-dialog {
  /* A STATED width, not `fit-content`. Sized to the content this card measured
     476px and held 598px of groups in a 542px body — 56px below the fold on a
     1280x720 laptop, the same defect `dialog-fit.spec.mjs` was written for. The
     four groups are short and the card was narrow, so the fix is the same balanced
     two-column flow Page setup now uses, and that needs a width that fits two
     `.dialog-field-grid` cells per column (2 x 144px plus the gaps). */
  width: min(620px, 100%);
  max-inline-size: min(100%, 76ch);
}

/* Balanced across two columns, for the reason `.page-setup-fields` records: a grid
   would align rows and pin a short group's row to a tall neighbour's height. */
.header-footer-settings-body {
  min-width: 0;
  columns: 2;
  column-gap: clamp(20px, 2.4vw, 28px);
}

.header-footer-settings-body > .dialog-group {
  margin-bottom: 20px;
  break-inside: avoid;
}

/* Apply to and Position run the FULL width rather than living in a column.
   Measured: at two columns this card gives each one 274px, and a
   `.dialog-field-grid` cell is 144px — so "Header from top" and "Footer from
   bottom" stacked, which is precisely the outcome `.dialog-field-grid`'s own
   comment refuses for a pair of short number fields. Spanning gives that pair the
   whole 588px and keeps it side by side, and the two taller groups below then
   balance against each other, which is what the two columns are for. */
.header-footer-settings-body > .dialog-group-span {
  column-span: all;
}

/* Rhythm on the ROWS and not as a gap on the fieldset, for the reason
   `.watermark-body` records: `fieldset { display: flex }` takes the legend out of
   the flow inconsistently across engines, and these legends are what a screen
   reader reads the checkboxes and radios out under. */
.header-footer-settings-body .dialog-check + .dialog-check,
.header-footer-settings-body .dialog-check + .dialog-note,
.header-footer-settings-body .dialog-check + .dialog-field,
.header-footer-settings-body .dialog-field-grid + .dialog-check {
  margin-top: 12px;
}

/* "Start at" and its spinner read across, so the number belongs to the words
   beside it rather than sitting under them. */
.header-footer-settings-body .dialog-check {
  flex-wrap: wrap;
}

/* The reason a disabled control carries, under the control it explains. Indented
   to the checkbox's text so it reads as that row's footnote and not as a note
   about the whole group. */
#headerFooterLinkReason {
  margin-top: 6px;
  padding-inline-start: 26px;
}

/* ---- Drop cap ------------------------------------------------------------- */
.drop-cap-dialog {
  /* `fit-content`, not the unparseable `min()` — see `.field-dialog`. */
  width: fit-content;
  max-inline-size: min(100%, 52ch);
}

.drop-cap-body {
  display: grid;
  gap: var(--space-4);
}

.drop-cap-options {
  display: grid;
  grid-template-columns: repeat(3, minmax(92px, 1fr));
  gap: var(--space-2);
}

.drop-cap-choice {
  position: relative;
  display: grid;
  min-width: 0;
  gap: var(--space-2);
  padding: var(--space-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  color: var(--muted);
  font-size: var(--fs-small);
  cursor: pointer;
}

.drop-cap-choice:has(input:checked) {
  border-color: var(--accent-text);
  background: var(--accent-soft);
  color: var(--ink);
}

/* `outline-offset: 2px` outside an already-accent 1px border drew TWO
   concentric blue rings around the selected tile — and the dialog focuses the
   checked radio as it opens, so that is what it looks like every single time it
   is used. One ring, seated on the border. */
.drop-cap-choice:focus-within {
  outline: 2px solid var(--accent-text);
  outline-offset: -1px;
}

.drop-cap-choice input {
  position: absolute;
  opacity: 0;
}

/* `min-height`, never `height`. The swatch holds a 47px letter whose LINE BOX is
   the font's business, not ours: Georgia renders it 48px tall, and the Linux CI
   runner — which has no Georgia and falls back to its own serif — renders it 54.
   A fixed 52px box therefore fit on one platform and clipped on the other, which
   is how the previous attempt at this passed locally and failed in CI. Sizing to
   the content instead is correct for any face, which is the only thing that can
   be true on every platform we run on. */
.drop-cap-sample {
  display: grid;
  min-height: 52px;
  grid-template-columns: 24px 1fr;
  align-items: start;
  overflow: hidden;
  color: var(--ink);
}

.drop-cap-sample b {
  align-self: start;
  font-family: Georgia, serif;
  font-size: 20px;
  font-weight: 600;
  line-height: 1;
}

.drop-cap-sample i {
  height: 38px;
  margin-top: 3px;
  background: repeating-linear-gradient(to bottom, var(--line-strong) 0 2px, transparent 2px 11px);
}

/* `auto`, not a pixel column: the 47px sample glyph measures ~36px in Georgia
   and was painting over the ruled lines beside it out of a 24px column — the
   opposite of what a drop cap does, which is RESERVE the width the text then
   wraps around. A fixed column also cannot be right on both platforms, since
   the Linux runner has no Georgia and falls back to a serif of its own metrics.
   Letting the column size to the letter is correct for any face.
   `line-height: normal` for the same reason: 0.92 made the box shorter than the
   glyph's own line box, so the element reported content taller than its box. */
.drop-cap-sample[data-mode="drop"],
.drop-cap-sample[data-mode="margin"] {
  grid-template-columns: auto 1fr;
}

.drop-cap-sample[data-mode="drop"] b,
.drop-cap-sample[data-mode="margin"] b {
  grid-row: span 2;
  font-size: 47px;
  line-height: normal;
}

.drop-cap-sample[data-mode="margin"] {
  overflow: visible;
}

.drop-cap-sample[data-mode="margin"] b {
  transform: translateX(-5px);
}

.drop-cap-sample[data-mode="margin"] i {
  padding-left: 5px;
  border-left: 1px solid var(--line-strong);
  background-clip: content-box;
}

/* Sized by what it holds, like `.number-control`: this takes 1 to 10, and it
   was a 128px box for two digits. The allowance is four characters plus the
   input's own padding plus Chromium's spin buttons, which is the same
   arithmetic `.number-control` documents.

   The label sits BESIDE the value rather than above it. `.dialog-field` stacks
   its label, which is right for a full-width text input and wrong for a
   two-digit one: it spent a 60px row on a 69px control and left 267px of that
   row empty. Word's Drop Cap dialog writes it inline — "Lines to drop: [3]" —
   and so does every options row in this product's own menus (`.menu-row`).
   Grid rather than flex so the bare "Lines" text node becomes a real item. */
.drop-cap-lines {
  display: grid;
  width: fit-content;
  grid-template-columns: max-content max-content;
  align-items: center;
  gap: 0 var(--space-3);
}

.drop-cap-lines > input {
  inline-size: calc(4ch + 2.25rem);
}

.drop-cap-lines:has(input:disabled) {
  opacity: 0.55;
}

.watermark-body {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 20px;
}

/* The rhythm inside a fieldset is carried by margins on the rows, not by a gap
   on the box: `fieldset { display: flex }` takes the legend out of the flow
   inconsistently across engines, and this dialog's legends are load-bearing
   (they are what a screen reader reads the radio group out under). */
.watermark-body .dialog-check + .dialog-check,
.watermark-body .dialog-field + .dialog-field-grid,
.watermark-body .dialog-field-grid + .dialog-check,
.watermark-body .dialog-check + .segmented {
  margin-top: 12px;
}

/* The kind radios read across. Three short phrases stacked cost 84px and left
   ~320px empty beside each one; on one line they cost 20px and the whole
   decision — none, text, picture — is one glance instead of three. They wrap to
   a second line on their own when the card is narrower than they are, which is
   what happens at 400px. */
.watermark-kinds {
  display: flex;
  flex-wrap: wrap;
  gap: 10px clamp(14px, 2.4vw, 22px);
}

/* The stacked-row rhythm above does not apply inside a row. */
.watermark-kinds .dialog-check + .dialog-check {
  margin-top: 0;
}

/* Font, Size and Colour on ONE row. The shared `.dialog-field-grid` gives every
   cell a 9rem floor, which is right for a pair of margin spinners and wrong for
   a colour well: three 9rem tracks plus gaps need 456px and the body is 415px,
   so the grid wrapped and Colour took a 60px row of its own with a 216px swatch
   in it — a control stretched to its container, which is the defect the last
   pass removed everywhere else. Sized to content here: the selects already
   report their widest option (`.dialog-body .dialog-select`), and the well is
   sized by the swatch rather than by a label it does not have. */
#watermarkTextFields .dialog-field-grid {
  grid-template-columns: minmax(0, max-content) minmax(0, max-content) max-content;
}

/* A disabled choice is still carrying a sentence — the reason the picture half
   is not offered — so it reads as unavailable rather than as absent. */
.watermark-body .dialog-check:has(input:disabled) {
  color: var(--muted);
  cursor: not-allowed;
}

/* The fields greyed while "No watermark" is chosen. `fieldset[disabled]` already
   stops every control inside it, which is the behaviour; this is what says so. */
#watermarkTextFields:disabled {
  opacity: 0.55;
}

#watermarkColor {
  /* A swatch, not a field: it has no text to fit, so it is sized by the
     ~3.5rem of colour a user needs to see rather than by the cell around it. */
  inline-size: 3.5rem;
  height: var(--h-field);
  padding: 2px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
}

/* ---- Table properties inspector ------------------------------------------ */
.table-properties-grid {
  display: flex;
  min-width: 0;
  flex-direction: column;
}

.table-property-section {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 12px;
  padding: 15px 0 17px;
  border-bottom: 1px solid var(--line);
}

.table-property-section legend {
  margin: 0;
  padding: 0;
}

/* Bound to the field above it at the shared 6px, against this section's own
   12px stack gap. */
.table-property-section > .dialog-field-note {
  margin-top: -6px;
}

.table-property-section:first-child {
  padding-top: 0;
}

.table-property-section:last-child {
  padding-bottom: 4px;
  border-bottom: none;
}

/* `.dialog-field select` is here too, because the object inspector builds five of
   them from a template string with no class, and they were rendering raw OS chrome
   in the middle of a product panel (docs/114 §6). */
.dialog-select,
.dialog-field select {
  width: 100%;
  height: var(--h-field);
  padding: 0 30px 0 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-body);
}

.dialog-select:focus,
.dialog-field select:focus {
  outline: none;
  border-color: var(--accent-text);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* A control is as wide as the choices it holds, not as wide as the dialog.
 *
 * Measured: Settings' Theme toggle was three 155px buttons spanning 476px to
 * hold "System", "Light" and "Dark"; Page setup's Orientation was two 148px
 * buttons; the Section dropdown was 303px to show "Section 1" and the Language
 * dropdown 476px to show "System default (English)". This is the same defect
 * the number spinners had before the last pass — a control stretched to the
 * container instead of sized to its content — and it is most of why these
 * dialogs read as big without being informative. Word and Docs size both of
 * these to their content.
 *
 * Scoped to `.dialog-body` so the side panels, whose narrow measure genuinely
 * is the control's width, are untouched. */
.dialog-body .segmented {
  width: fit-content;
  max-width: 100%;
}

.dialog-body .segmented button {
  flex: 0 1 auto;
  padding: 0 14px;
}

.dialog-body .dialog-select,
.dialog-body .dialog-field > select {
  /* `fit-content` on a select is its widest OPTION, so the box still fits every
     value it can show. The floor keeps a one-digit list ("Columns ▸ Number")
     from collapsing to a stub, and `100%` is the ceiling in a narrow cell. */
  inline-size: fit-content;
  min-inline-size: 11ch;
  max-inline-size: 100%;
}

.dialog-check {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  color: var(--ink);
  font-size: var(--fs-small);
  cursor: pointer;
}

.dialog-check input {
  margin: 2px 0 0;
  accent-color: var(--accent);
}

.dialog-check span {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 2px;
}

.dialog-check strong {
  font-weight: 600;
}

.dialog-check small,
.dialog-field-note {
  color: var(--muted);
  font-size: var(--fs-caption);
  font-weight: 450;
  line-height: 1.35;
}

/* A POSITIVE bind distance. `-4px` was written to claw back a flex `gap` that
   the note's container happened to apply, and it is wrong wherever there is no
   gap to claw back: in "Create a style" the body is a plain block, so the note
   was pulled four pixels up and painted across the bottom edge of the input it
   describes, straight through the focus ring. The two flex forms that DO carry
   a gap (`.link-body`, `.bookmark-add`) pull the note back to this same 6px
   themselves, via `--dlg-bind`, so the distance is now one number rather than a
   correction to a number. */
.dialog-field-note {
  margin: 6px 0 0;
}

/* The panel's own 6px gap already places this one. */
.link-mode-panel > .dialog-field-note {
  margin-top: 0;
}

/* ---- Bookmark manager ---------------------------------------------------- */
/* Measured at 1280x720: a 460px card whose body's max-content is 338px — the
 * "No bookmarks yet" line, which is the widest thing in it — so a quarter of
 * every row was empty. Word's Bookmark dialog is about 360px wide and Google
 * Docs' is a narrow list; a bookmark name is a few words, not a sentence.
 *
 * `fit-content` so a document with three short bookmarks gets a small card, and
 * a ch ceiling so a 200-character bookmark name cannot drag it out — the name
 * is ellipsised at any width, but `text-overflow` does not shrink an element's
 * max-content contribution, so without the ceiling one long name would set the
 * card width. */
.bookmark-dialog {
  width: fit-content;
  max-inline-size: min(100%, 46ch);
}

.bookmark-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* Proofing languages (docs/146 §3, ADR-042): Word's Options ▸ Language table,
   one row per language. A real `<table>` rather than a list of flex rows, because
   the columns are headed and a screen reader reads "Proofing, Installed" from the
   header association rather than from a guess. */
.proof-lang-dialog {
  width: fit-content;
  max-inline-size: min(100%, 60ch);
}

.proof-lang-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.proof-lang-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-body);
}

.proof-lang-table th,
.proof-lang-table td {
  padding: 6px 8px;
  text-align: left;
  vertical-align: middle;
}

.proof-lang-table thead th {
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.proof-lang-row + .proof-lang-row th,
.proof-lang-row + .proof-lang-row td {
  border-top: 1px solid var(--line);
}

.proof-lang-name {
  font-weight: 550;
}

.proof-lang-state,
.proof-lang-size,
.proof-lang-words {
  color: var(--muted);
  white-space: nowrap;
}

.proof-lang-actions {
  text-align: right;
}

.proof-lang-action {
  white-space: nowrap;
}

/* Insert-field picker: a compact list of field kinds, styled to match the
   bookmark manager's rows and the graphite dialog chrome. */
.field-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.field-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 360px;
  margin: 0;
  padding: 0;
  overflow: auto;
  list-style: none;
  overscroll-behavior: contain;
}

/* 58px per row for a label and a six-word note put 350px of list in a 500px
   dialog. A picker's rows are the densest thing in either reference product —
   Word's Field dialog is a plain listbox — so the row keeps its note and loses
   the air around it. */
.field-choice {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: 6px 8px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.field-choice:hover {
  background: var(--bg-2);
}

.field-choice:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

.field-choice-icon {
  flex: 0 0 auto;
  color: var(--muted);
  font-size: 19px;
}

.field-choice:hover .field-choice-icon {
  color: var(--accent-text);
}

.field-choice-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.field-choice-label {
  font-size: var(--fs-body);
  font-weight: 550;
}

.field-choice-note {
  color: var(--muted);
  font-size: var(--fs-small);
  font-weight: 450;
}

/* ---- Insert ▸ Symbol / Emoji pickers ------------------------------------- */
.glyph-panel-content {
  display: flex;
  min-height: 0;
  flex: 1;
  flex-direction: column;
}

/* Two lines of heading. No padding and no `height: auto` of its own any more:
 * `.panel-head` is `min-height` plus `var(--pnl-pad-*)`, so a head that needs
 * to grow simply grows and keeps the shell's inset (HF-265 D2). */
.glyph-panel .panel-head {
  min-height: 58px;
  align-items: flex-start;
}

.glyph-panel .panel-head .dialog-heading {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 9px;
}

.glyph-panel .panel-head small {
  display: block;
  color: var(--muted);
  font-size: var(--fs-caption);
  line-height: 1.3;
}

.glyph-panel-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  /* The shell's inset — see `.version-foot`. */
  padding: var(--pnl-pad-y) var(--pnl-pad-x);
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-caption);
}

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

.glyph-search {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--bg-2);
  color: var(--ink);
  font: inherit;
}

.glyph-search:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

.glyph-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.glyph-tab {
  padding: 4px 10px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-small);
  line-height: 1.4;
  cursor: pointer;
}

.glyph-tab:hover {
  background: var(--bg-2);
  color: var(--ink);
}

.glyph-tab:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

.glyph-tab.is-active {
  background: var(--accent-soft, var(--bg-2));
  border-color: var(--accent-text);
  color: var(--ink);
}

.glyph-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
  gap: 4px;
  max-height: 320px;
  overflow: auto;
  padding: 2px;
  overscroll-behavior: contain;
}

.glyph-cell {
  display: grid;
  place-items: center;
  aspect-ratio: 1 / 1;
  min-height: 44px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}

.glyph-grid-emoji .glyph-cell {
  font-size: 24px;
}

.glyph-cell:hover {
  background: var(--bg-2);
  border-color: var(--line);
}

.glyph-cell:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

.glyph-empty {
  margin: 0;
  padding: 12px 4px;
  color: var(--muted);
  font-size: var(--fs-small);
}

/* ---- Insert/edit link dialog --------------------------------------------- */
.link-dialog {
  width: min(460px, 100%);
}

/* One rhythm for a stacked form, shared with `.bookmark-add` below.
 *
 * Measured, before: EVERY vertical gap in this dialog was 12px. "Text to
 * display" sat exactly as far from its own input as that input sat from the
 * next label, so nothing in the form was grouped — the eye had to read the
 * words to work out which label belonged to which box. `.dialog-field`, which
 * the rest of the dialogs use, has bound its label at 6px all along; this
 * dialog and the bookmark manager are the two that build their labels by hand
 * and so never inherited it.
 *
 * So: 16px between rows, and the element directly after a label is pulled back
 * to 6px — the same 6/16 the `.dialog-field` + `.properties-grid` pair produce,
 * and the same ratio Word and Docs use. A helper note is bound to the control
 * ABOVE it by the same amount, because that is what it is about. */
.link-body,
.bookmark-add {
  /* Pulls a bound element back from the row gap to the 6px label distance. */
  --dlg-bind: calc(6px - var(--space-4));
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.link-body > label + *,
.link-body > .dialog-field-note,
.bookmark-add > label + *,
.bookmark-add > .dialog-field-note {
  margin-top: var(--dlg-bind);
}

.link-body > label {
  color: var(--muted);
  font-size: var(--fs-small);
  font-weight: 600;
}

.link-optional {
  color: var(--muted);
  font-weight: 450;
}

.link-field-label {
  color: var(--muted);
  font-size: var(--fs-small);
  font-weight: 600;
}

/* "Link to" beside its tab strip instead of over it. `wrap`, not `nowrap`:
   at 400px, or with a translation that makes the two tab labels longer than
   the card, the strip drops to its own line rather than being squeezed — which
   is the stacked layout this replaces, reached only when it is needed. */
.link-mode-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.link-body input,
.link-body select {
  width: 100%;
  height: var(--h-field);
  padding: 0 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  /* `background-color`, NOT the `background` shorthand: this rule is (0,1,1) and the
     shared select treatment is (0,1,0), so the shorthand would win on specificity
     regardless of order and reset the caret image to none. `styles-control.spec.mjs`
     caught exactly that here, from computed style. */
  background-color: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-body);
  font-weight: 450;
}

/* Room on the right for the shared select caret. After the shorthand above, which
   would otherwise reset it (same specificity, later wins). */
.link-body select {
  padding-right: 26px;
}

.link-body input:focus,
.link-body select:focus {
  outline: none;
  border-color: var(--accent-text);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* Sized to its two choices, for the same reason as `.dialog-body .segmented`:
   this is the same control idiom under a different class, and it was a 416px
   strip holding two labels. */
.link-mode {
  display: flex;
  width: fit-content;
  max-width: 100%;
  gap: 4px;
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--bg-2);
}

.link-mode-tab {
  flex: 0 1 auto;
  height: var(--h-search);
  padding: 0 10px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-small);
  font-weight: 600;
  cursor: pointer;
}

.link-mode-tab:hover {
  color: var(--ink);
}

.link-mode-tab.is-active {
  border-color: var(--line-strong);
  background: var(--surface);
  color: var(--ink);
}

.link-mode-tab:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

.link-mode-panel {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* "ScreenTip (optional)" was painting as "ScreenTip(optional)": the space
   before this span is the label's trailing whitespace, and it collapses away at
   the end of the text run. The gap belongs to the word, so it is set here. */
.link-optional {
  margin-inline-start: 0.35em;
}

#linkDialogNote.error,
#linkPlaceEmpty {
  color: var(--danger);
  font-weight: 550;
}

#linkPlaceEmpty {
  color: var(--muted);
  font-weight: 450;
}

/* Layout and rhythm share `.link-body`'s rule above. */
.bookmark-add > label {
  color: var(--muted);
  font-size: var(--fs-small);
  font-weight: 600;
}

.bookmark-add-row {
  display: flex;
  gap: var(--space-2);
}

.bookmark-add-row > input {
  flex: 1 1 auto;
  min-width: 0;
  height: var(--h-field);
  padding: 0 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-body);
  font-weight: 450;
}

.bookmark-add-row > input:focus {
  outline: none;
  border-color: var(--accent-text);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.bookmark-add .dialog-button {
  flex: 0 0 auto;
}

#bookmarkAddNote.error {
  color: var(--danger);
  font-weight: 550;
}

.bookmark-list-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  color: var(--muted);
  font-size: var(--fs-caption);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.bookmark-sort {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 7px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-caption);
  font-weight: 600;
  letter-spacing: 0.02em;
  cursor: pointer;
}

.bookmark-sort:hover {
  background: var(--bg-2);
  color: var(--ink);
}

.bookmark-sort .ms {
  font-size: 15px;
}

.bookmark-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 320px;
  margin: 0;
  padding: 0;
  overflow: auto;
  list-style: none;
  overscroll-behavior: contain;
}

.bookmark-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 4px 4px 4px 8px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
}

.bookmark-row:hover {
  background: var(--bg-2);
}

.bookmark-row.editing {
  background: var(--bg-2);
  border-color: var(--line);
}

.bookmark-goto {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
  padding: 5px 6px;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-body);
  font-weight: 550;
  text-align: left;
  cursor: pointer;
}

.bookmark-goto .ms {
  flex: 0 0 auto;
  color: var(--muted);
  font-size: 17px;
}

.bookmark-goto:hover {
  color: var(--accent-text);
}

.bookmark-goto:hover .ms {
  color: var(--accent-text);
}

.bookmark-goto:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

.bookmark-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bookmark-rename-input {
  flex: 1 1 auto;
  min-width: 0;
  height: 30px;
  padding: 0 8px;
  border: 1px solid var(--accent-text);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-body);
  font-weight: 550;
}

.bookmark-rename-input:focus {
  outline: none;
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.bookmark-row-actions {
  display: flex;
  flex: 0 0 auto;
  gap: 2px;
}

.bookmark-action {
  display: grid;
  width: 28px;
  height: 28px;
  place-items: center;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.bookmark-action .ms {
  font-size: 17px;
}

.bookmark-action:hover {
  background: var(--surface);
  color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--line);
}

.bookmark-action.danger:hover {
  color: var(--danger);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--danger-line) 40%, transparent);
}

.bookmark-action:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

.bookmark-empty {
  margin: 0;
  /* The body's own 16px gap already separates this from the list head; 18px on
     top of it opened a 34px void above one sentence. */
  padding: var(--space-2) 4px var(--space-3);
  color: var(--muted);
  font-size: var(--fs-small);
  text-align: center;
}

/* The legend's typography lives with the shared group heading, up beside
   `.settings-label`, so the two cannot drift apart again. */
.dialog-group {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: none;
}

.dialog-field-grid {
  display: grid;
  /* Pairs (Top/Bottom, Left/Right) stay side by side wherever two will fit,
     and the cells are as wide as their content rather than half the dialog.
     The old `@media (max-width: 620px) { 1fr }` override made phones WORSE by
     stacking two 5-character fields into one 302px-wide column each; Word and
     Docs keep those pairs paired at phone widths. */
  grid-template-columns: repeat(auto-fit, minmax(clamp(7rem, 20vw, 9rem), max-content));
  justify-content: start;
  gap: clamp(8px, 1vw, 12px);
}

.orientation-segmented button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
}

.orientation-segmented .ms {
  font-size: var(--icon-control);
}

.number-control {
  display: grid;
  /* A margin or a column count is at most five characters wide. Stretching the
     box to `1fr` made it 182px at 1280 and 302px at 390 — wider on the phone,
     where space is scarcest. Sized to its content instead, with the stepper
     keeping its own fixed column. */
  inline-size: fit-content;
  /* Five characters ("22.00"), the input's own 14px of padding, AND the native
     spin buttons Chromium lays out inside the input — roughly 17px, which the
     old `1.25rem` allowance did not include. An A4 height of 11.69 needed 66px
     of content in the 60px box that produced, so the last digit was cut off.
     Same defect as the dialog button below: a fixed box, a label that grew. */
  grid-template-columns: calc(5ch + 2.25rem) 32px;
  height: var(--h-field);
  overflow: hidden;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.number-control input {
  width: 100%;
  min-width: 0;
  padding: 0 4px 0 10px;
  border: none;
  outline: none;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-body);
  font-variant-numeric: tabular-nums;
}

/* The unit sits INSIDE the field, not in a filled compartment beside it. A
   ruled, shaded box around "in" made every measurement look like two controls
   where there is one value, and Page setup alone shows six of them. Word and
   Docs both render the unit as quiet text inside the field. */
.number-control > span {
  display: grid;
  place-items: center;
  padding-inline-end: 2px;
  color: var(--muted);
  font-size: var(--fs-small);
  font-weight: 500;
}

.oss {
  font-size: var(--fs-caption);
  color: var(--faint);
}

.link {
  border: none;
  background: none;
  color: var(--accent-text);
  font-weight: 600;
  cursor: pointer;
  padding: 0;
}

/* ---- Right-click context menu ---------------------------------------------- */
.context-menu {
  position: fixed;
  /* Above the floating selection toolbar (z-index 40) so a right-click on a
     selection never has its menu occluded — or its items click-intercepted — by
     the toolbar that hovers the same selection. */
  z-index: var(--z-menu);
  min-width: 224px;
  max-width: min(340px, calc(100vw - 16px));
  max-height: calc(100vh - 16px);
  overflow: auto;
  padding: 5px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-popover);
  box-shadow: var(--shadow-popover);
  color: var(--ink);
  overscroll-behavior: contain;
}

.context-menu[hidden] {
  display: none;
}

.menu-item {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 10px;
  width: 100%;
  text-align: left;
  padding: 5px 9px;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  font-size: var(--fs-body);
  cursor: pointer;
  white-space: nowrap;
}

/* Leading-icon gutter — reserved on every row so labels align whether or not
   a row carries an icon (Word / Google Docs convention). */
.menu-item-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  opacity: 0.72;
}

.menu-item.active .menu-item-icon,
.menu-item:hover .menu-item-icon,
.menu-item:focus-visible .menu-item-icon {
  opacity: 1;
}

/* Submenu affordance + the flyout that opens to the side. */
.menu-item-caret {
  justify-self: end;
  color: var(--faint);
  font-size: 15px;
  line-height: 1;
}

.menu-item.active .menu-item-caret,
.menu-item:hover .menu-item-caret,
.menu-item:focus-visible .menu-item-caret {
  color: inherit;
}

/* Keep a submenu-parent lit while focus is inside its open flyout, so the
   open path stays visible. */
.menu-item.has-submenu[aria-expanded="true"] {
  background: var(--accent-soft);
  color: var(--accent-text);
}

.editor-submenu {
  z-index: var(--z-submenu);
}

.menu-item:hover,
.menu-item.active,
.menu-item:focus-visible {
  background: var(--accent-soft);
  color: var(--accent-text);
  outline: none;
}

.menu-item.danger:hover,
.menu-item.danger.active,
.menu-item.danger:focus-visible {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  color: var(--danger);
}

.menu-item:disabled {
  cursor: not-allowed;
  color: var(--faint);
  background: transparent;
}

.menu-item-label {
  overflow: hidden;
  text-overflow: ellipsis;
}

.menu-item-hint {
  max-width: 150px;
  overflow: hidden;
  color: var(--faint);
  font-size: var(--fs-small);
  text-overflow: ellipsis;
}

/* Disabled rows are greyed in place (Google Docs style): the label, any
   shortcut hint, and the leading icon all mute, and no reason text is shown. */
.menu-item:disabled .menu-item-icon {
  opacity: 0.4;
}

.menu-item:disabled .menu-item-hint {
  color: var(--faint);
}

.menu-divider {
  height: 1px;
  margin: 4px 6px;
  background: var(--line);
}

.table-style-menu {
  min-width: 220px;
  display: grid;
  gap: 4px;
}

.table-style-choice {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  border: 0;
  border-radius: var(--radius-sm);
  padding: 7px 9px;
  background: transparent;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}

.table-style-choice:hover {
  background: var(--accent-soft);
}

.table-style-swatch {
  display: grid;
  grid-template-rows: repeat(3, 4px);
  width: 42px;
  overflow: hidden;
  border: 1px solid var(--line-strong);
  border-radius: 3px;
}

.table-style-swatch i:nth-child(1) { background: var(--accent); }
.table-style-swatch i:nth-child(2) { background: color-mix(in srgb, var(--accent) 18%, var(--surface)); }
.table-style-swatch i:nth-child(3) { background: var(--surface-alt); }
.table-style-clear { font-size: var(--fs-small); }

/* ---- Line & paragraph spacing menu ----------------------------------------- */
.spacing-menu {
  /* Wide enough for "Remove space before paragraph" — Word's and Docs' own
     wording — because a row truncated to "R…" is not a row. */
  min-width: 264px;
}

/* An explanation under a field, for the state where the field is empty because
   the value comes from somewhere else. `--faint` is the same tone the menu's
   own headings use. */
.menu-note {
  margin: 0;
  padding: 0 10px 6px 37px;
  color: var(--faint);
  font-size: var(--fs-small);
  line-height: 1.35;
}

/* A control showing a value the paragraph INHERITED from its style rather than
   one it set itself. The number is the real, effective number — Word shows the
   resolved value and Docs ticks the resolved preset — so this cannot be a
   greyed-out "nothing here" treatment; it is an italic that says "not written
   on this paragraph", with the sentence under the field carrying the meaning and
   `--muted` (not `--faint`) keeping the value itself at AA against the menu. */
.spacing-menu [data-from-style] {
  font-style: italic;
  color: var(--muted);
}

.table-format-menu {
  width: min(286px, calc(100vw - 16px));
  max-height: calc(100vh - 16px);
  overflow: auto;
  overscroll-behavior: contain;
}

.menu-heading {
  padding: 4px 10px 6px;
  font-size: var(--fs-caption);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--faint);
}

.table-context {
  padding: 0 10px 6px;
  color: var(--muted);
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
}

/* Line-spacing rows carry a leading check column; the check shows on the
   currently-applied value. */
.spacing-line {
  display: flex;
  align-items: center;
  gap: 8px;
}

.menu-check {
  width: 14px;
  height: 14px;
  flex: 0 0 14px;
  opacity: 0;
  background: currentcolor;
  -webkit-mask: var(--check-svg) center / contain no-repeat;
  mask: var(--check-svg) center / contain no-repeat;
  --check-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z'/%3E%3C/svg%3E");
}

.spacing-line[aria-checked="true"] .menu-check {
  opacity: 1;
}

.spacing-line[aria-checked="true"] {
  color: var(--accent-text);
}

.menu-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  font-size: var(--fs-body);
  color: var(--ink);
}

.menu-row input {
  margin-left: auto;
  width: 56px;
  height: 26px;
  padding: 0 8px;
  text-align: right;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-variant-numeric: tabular-nums;
}

.menu-row input:focus {
  outline: none;
  border-color: var(--accent-text);
}

.menu-row .menu-select {
  margin-left: auto;
}

.menu-select {
  height: 26px;
  padding: 0 6px;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
}

.menu-select:focus {
  outline: none;
  border-color: var(--accent-text);
}

.menu-unit {
  color: var(--muted);
  font-size: var(--fs-small);
  width: 14px;
}

/* ---- Paragraph-options menu (shading + line/page-break flags) --------------- */
.menu-check-row {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 6px 10px;
  font-size: var(--fs-body);
  color: var(--ink);
  cursor: pointer;
  border-radius: 6px;
}

.menu-check-row:hover {
  background: var(--accent-soft);
}

.menu-check-row input {
  width: 15px;
  height: 15px;
  accent-color: var(--accent);
  cursor: pointer;
}

.shade-row {
  justify-content: space-between;
}

.shade-pick {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
}

.shade-pick input[type="color"] {
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: none;
  cursor: pointer;
}

.chip-btn {
  height: 26px;
  padding: 0 12px;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: var(--surface);
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-small);
  cursor: pointer;
}

.chip-btn:hover {
  background: var(--accent-soft);
  color: var(--accent-text);
  border-color: var(--accent-text);
}

.chip-row {
  display: flex;
  gap: 4px;
}

/* ---- Insert-table grid picker ---------------------------------------------- */
.grid-picker {
  padding: 8px;
}

.grid-cells {
  display: grid;
  grid-template-columns: repeat(10, 15px);
  gap: 2px;
}

.grid-cells .gc {
  width: 15px;
  height: 15px;
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  background: var(--surface);
  cursor: pointer;
  padding: 0;
}

.grid-cells .gc.on {
  background: var(--accent-soft);
  border-color: var(--accent-text);
}

.grid-label {
  margin-top: 7px;
  text-align: center;
  font-size: var(--fs-small);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* A finger cannot hit a 15px cell. Drawn for a mouse — Docs' own hover grid is
   about this size — and measured at 15x15 on a phone, under this file's own
   floor: WCAG 2.5.8 Target Size (Minimum), Level AA, which `phone_chrome.mjs`
   exports as `MIN_TOUCH_TARGET_PX = 24`. Keyed on the POINTER, like every other
   touch rule here, so a touchscreen laptop gets the same grid as a phone. Ten
   24px columns and nine 2px gaps are 258px: an anchored popover still fits a
   320px window with its 8px gutters. */
@media (pointer: coarse) {
  .grid-cells {
    grid-template-columns: repeat(10, 24px);
  }

  .grid-cells .gc {
    width: 24px;
    height: 24px;
  }
}

/* ---- Command palette (⌘K) -------------------------------------------------- */
.cmd-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-palette);
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding-top: 12vh;
  background: rgba(20, 22, 28, 0.34);
}

.cmd-overlay[hidden] {
  display: none;
}

.cmd-box {
  width: min(560px, 92vw);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-popover);
  box-shadow: var(--shadow-popover);
  overflow: hidden;
}

.cmd-search {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--line);
}

.cmd-search-icon {
  color: var(--muted);
  font-size: var(--icon-control);
  flex: 0 0 auto;
}

.cmd-search input {
  flex: 1;
  /* A flex item defaults to `min-width: auto`, which refuses to shrink below the
     item's INTRINSIC width — and a text input's intrinsic width is its character
     `size`, not its content. So this row's width depended on the platform's font
     metrics: it fitted a 320px window on macOS (input 187px, row scrollWidth 292
     == clientWidth) and overflowed on CI's Linux faces (scrollWidth 366 against a
     292px client), which is how a genuine sideways scroller reached `main` while
     every local run was green. `min-width: 0` is the textbook fix and makes the
     row's width depend on the window instead of on the font. */
  min-width: 0;
  border: none;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-medium);
  outline: none;
}

.cmd-kbd {
  font-size: var(--fs-caption);
  color: var(--faint);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 2px 6px;
}

.cmd-list {
  max-height: 340px;
  overflow: auto;
  padding: 6px;
  overscroll-behavior: contain;
}

.cmd-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-body);
  text-align: left;
  cursor: pointer;
}

.cmd-item .cmd-group {
  color: var(--faint);
  font-size: var(--fs-small);
}

.cmd-item .cmd-hint {
  margin-left: auto;
  color: var(--faint);
  font-size: var(--fs-small);
}

.cmd-item.sel,
.cmd-item:hover {
  background: var(--accent-soft);
  color: var(--accent-text);
}

.cmd-empty {
  padding: 16px;
  text-align: center;
  color: var(--faint);
  font-size: var(--fs-body);
}

/* ---- Find / replace -------------------------------------------------------- */
.find-panel {
  position: fixed;
  top: calc(var(--h-header) + 88px);
  right: 18px;
  z-index: var(--z-find);
  display: flex;
  flex-direction: column;
  width: min(440px, calc(100vw - 36px));
  max-height: calc(100vh - var(--h-header) - 104px);
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-popover);
  box-shadow: var(--shadow-popover);
}

.find-panel[hidden] {
  display: none;
}

/* Find and replace keeps its GEOMETRY — fixed, top-right, so it does not cover
 * the text you are searching, which is Word's and Google Docs' model and not a
 * defect — but takes the shell's PADDING from the same pair as every other
 * panel. It was already the only panel whose head and body agreed, and
 * `var(--space-2) var(--space-4)` is where `--pnl-pad-y`/`--pnl-pad-x` came
 * from; naming them here is what stops the two drifting apart again. The right
 * inset was `--space-3` and is now symmetric, as the dialog heads are. */
.find-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: 48px;
  padding: var(--pnl-pad-y) var(--pnl-pad-x);
  border-bottom: 1px solid var(--line);
}

.find-panel-head > div {
  display: flex;
  min-width: 0;
  align-items: baseline;
  gap: var(--space-2);
}

.find-panel-title {
  color: var(--ink);
  font-size: var(--fs-medium);
  font-weight: 650;
}

.find-panel-body {
  display: grid;
  min-height: 0;
  overflow: auto;
  gap: var(--space-3);
  padding: var(--pnl-pad-y) var(--pnl-pad-x);
  overscroll-behavior: contain;
}

.find-search-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--h-control) var(--h-control);
  align-items: center;
  gap: 6px;
}

.find-panel input[type="text"] {
  width: 100%;
  min-width: 0;
  height: var(--h-field);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  padding: 0 8px;
  font: inherit;
  font-size: var(--fs-body);
  outline: none;
}

.find-panel input[type="text"]:focus {
  border-color: var(--accent-text);
  box-shadow: 0 0 0 2px var(--accent-soft);
}

.find-panel .btn.icon-only {
  height: var(--h-control);
}

.find-status {
  color: var(--muted);
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.find-status.miss {
  color: var(--danger);
}

.find-status:empty {
  display: none;
}

.find-options {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.find-option {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: var(--h-control);
  padding: 4px 9px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-size: var(--fs-small);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}

.find-option:has(input:checked) {
  background: var(--accent-soft);
  color: var(--accent-text);
  border-color: var(--accent-text);
}

.find-option input {
  width: 14px;
  height: 14px;
  margin: 0;
  accent-color: var(--accent);
}

.find-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding-top: var(--space-1);
}

.chip-btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
  border-color: var(--accent);
}

.chip-btn-primary:hover {
  filter: brightness(1.05);
  background: var(--accent);
  color: var(--accent-ink);
  border-color: var(--accent);
}

/* ---- Floating selection toolbar -------------------------------------------- */
.sel-toolbar {
  position: fixed;
  z-index: var(--z-selection-toolbar);
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 4px 6px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-popover);
  box-shadow: var(--shadow-popover);
}

.sel-toolbar[hidden] {
  display: none;
}

.sel-divider {
  width: 1px;
  height: 20px;
  background: var(--line-strong);
  margin: 0 3px;
}

.sel-toolbar .fmt {
  color: var(--ink);
}

.sel-toolbar .fmt:hover:not(:disabled) {
  background: var(--bg-2);
}

.sel-toolbar .fmt[aria-pressed="true"] {
  color: var(--accent-text);
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

/* Text-color / highlight buttons on the floating bar: a glyph over a swatch bar,
   matching the ribbon's color-apply half, opening the same swatch popovers. */
.sel-toolbar .sel-color-btn {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: background 0.11s;
}

.sel-toolbar .sel-color-btn .color-apply-glyph {
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
}

.sel-toolbar .sel-color-btn .ms.color-apply-glyph {
  font-size: var(--icon-control);
  font-weight: 400;
}

.sel-toolbar .sel-color-btn:hover {
  background: var(--bg-2);
}

.sel-toolbar .sel-color-btn[aria-expanded="true"] {
  background: var(--accent-soft);
  color: var(--accent-text);
}

.sel-toolbar .sel-color-btn:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

/* A host-owned affordance for canvas-painted links. The engine supplies the
   exact authored range/target; this chip supplies visible, policy-checked
   activation without making DOM text authoritative. */
.link-chip {
  position: fixed;
  z-index: var(--z-link-chip);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  width: max-content;
  max-width: min(440px, calc(100vw - 24px));
  min-width: min(300px, calc(100vw - 24px));
  padding: 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-popover);
  background: var(--surface);
  box-shadow: var(--shadow-popover);
}

.link-chip[hidden] {
  display: none;
}

.link-chip-icon {
  display: grid;
  width: 30px;
  height: 30px;
  place-items: center;
  border-radius: var(--radius-sm);
  color: var(--accent-text);
  background: var(--accent-soft);
}

.link-chip-icon .ms {
  font-size: 17px;
}

.link-chip-copy {
  display: flex;
  min-width: 0;
  flex-direction: column;
  line-height: 1.25;
}

.link-chip-kind {
  color: var(--faint);
  font-size: var(--fs-caption);
  font-weight: 700;
  letter-spacing: 0.045em;
  text-transform: uppercase;
}

.link-chip-target {
  overflow: hidden;
  color: var(--ink);
  font-size: var(--fs-small);
  font-weight: 550;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.link-chip-action {
  height: 30px;
  padding: 0 11px;
  border: 1px solid var(--accent-line);
  border-radius: var(--radius-sm);
  color: var(--accent-text);
  background: var(--accent-soft);
  font-weight: 650;
  cursor: pointer;
}

.link-chip-action:hover:not(:disabled) {
  filter: brightness(1.05);
}

.link-chip-action:disabled {
  border-color: var(--line);
  color: var(--faint);
  background: var(--bg-2);
  cursor: default;
}

.link-chip-danger {
  color: var(--danger);
}

/* Border presets: a row of buttons each showing a mini box with the relevant
   edge(s) emphasized (CSS-drawn — no icon assets). */
.border-row {
  justify-content: space-between;
}

.border-presets {
  display: flex;
  gap: 2px;
}

.border-btn {
  display: grid;
  place-items: center;
  width: 27px;
  height: 27px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  cursor: pointer;
}

.border-btn:hover {
  background: var(--bg-2);
}

.border-btn[aria-pressed="true"] {
  background: var(--accent-soft);
  border-color: var(--accent-line);
}

.border-btn .bx {
  width: 15px;
  height: 15px;
  border: 1px solid var(--faint);
}

.border-btn[data-border="none"] .bx {
  border-style: dashed;
}

.border-btn[data-border="box"] .bx {
  border: 1.5px solid var(--ink);
}

.border-btn[data-border="top"] .bx {
  border-top: 2px solid var(--ink);
}

.border-btn[data-border="bottom"] .bx {
  border-bottom: 2px solid var(--ink);
}

.border-btn[data-border="left"] .bx {
  border-left: 2px solid var(--ink);
}

.border-btn[data-border="right"] .bx {
  border-right: 2px solid var(--ink);
}

.border-color input[type="color"] {
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: none;
  cursor: pointer;
}

/* ---- Ribbon (tabbed toolbar) ----------------------------------------------- */
/* Canvas/grid ribbon (redesign): full-bleed and flat — no floating rounded
   card. It is one horizontal band of the shell grid, separated from the bar
   above and the workarea below by single hairlines. */
.ribbon {
  background: var(--bg);
  border-radius: var(--radius);
  margin: 0;
  border: none;
  border-bottom: 0;
  border-radius: 0;
  overflow: hidden;
  z-index: 2;
}

/* The tab strip is NOT a row of its own any more — it sits in the header, in
   the slot the compact chrome gives its menu bar (docs/123 §3). So it carries
   no horizontal padding of its own (the header's 14px is the page gutter, and
   the strip aligns with the title above it) and no bottom rule (there is no
   band edge to draw here; the selected tab's own underline is the indicator).

   It scrolls under a trailing fade exactly as `.app-menu-bar` does, and for the
   same reason: the 58px header is fully committed, a visible scrollbar would
   have to come out of the strip's own height, and a name cut off with no cue is
   `109` HF-097. The chevron is a sibling, so it never scrolls away. */
.ribbon-nav {
  grid-area: nav;
  display: flex;
  align-items: center;
  gap: 2px;
  min-width: 0;
}

/* The strip takes the whole row so the chevron lands at the far right, where
   both references put the ribbon's fold control (Word's caret at the ribbon's
   right edge; Google Docs' `^` at the right end of its toolbar, measured).
   Left to shrink-wrap it sat flush against the last tab and read as a 9th one. */
.ribbon-nav .ribbon-tabs {
  flex: 1 1 auto;
}

.ribbon-tabs {
  display: flex;
  align-items: stretch;
  gap: 2px;
  height: var(--h-tab);
  min-width: 0;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-mask-image: linear-gradient(to right, black calc(100% - 22px), transparent);
  mask-image: linear-gradient(to right, black calc(100% - 22px), transparent);
}

.ribbon-tabs::-webkit-scrollbar {
  display: none;
}

/* Compact ↔ ribbon view toggle: collapses the whole band to just the tab strip
   (Word's "collapse the ribbon"), giving the document more room. */
.ribbon-view-toggle {
  margin-left: auto;
  align-self: center;
  display: inline-grid;
  place-items: center;
  width: 28px;
  height: 26px;
  border: 1px solid transparent;
  border-radius: 6px;
  background: none;
  color: var(--muted);
  cursor: pointer;
}

.ribbon-view-toggle:hover {
  background: var(--bg-2);
  color: var(--ink);
}

.ribbon-view-toggle .ms {
  font-size: 20px;
}

.ribbon.is-collapsed {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

.ribbon.is-collapsed .ribbon-body {
  display: none;
}

/* Compact view deletes the band, so the chrome's bottom edge moves up with it
   and every panel anchored to `--chrome-bottom` follows. `:has()` reads the
   ribbon's own state rather than requiring a second class on <body>, so the
   token can never disagree with what is actually on screen. */
body:has(.ribbon.is-collapsed) {
  /* The tab strip lives in the header now, so a collapsed ribbon contributes
     nothing at all and the chrome ends where the header ends. */
  --chrome-bottom: var(--h-header);
}

/* Compact chrome replaces the whole ribbon — tab strip AND band — with one
   44px toolbar, so the chrome's bottom edge moves up by more than the collapsed
   case above and every `--chrome-bottom`-anchored panel has to follow. Without
   this the object/size, paragraph and table panels sit ~80px too low, hanging
   off their own top edge.

   Matches the sibling rule's approach: read the state that is actually on
   screen (`body.compact-mode`, set by `setChromeMode`) so the token cannot
   disagree with the rendered chrome. */
body.compact-mode {
  /* Header plus the compact toolbar's own height and bottom hairline. */
  --chrome-bottom: calc(var(--h-header) + var(--h-compact-bar) + 14px);
}

/* 12px of side padding, which is ONLYOFFICE's own tab metric
   (`.toolbar .tabs li > a { padding: 0 12px }` — `toolbar.less`, and the same
   value in their shipped 9.4.1 stylesheet). It was 14px, chosen by nobody;
   across 8 tabs that is 32px the header does not have to spare. */
.ribbon-tab {
  height: var(--h-tab);
  padding: 0 12px;
  border: none;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-ribbon-tab);
  font-weight: 600;
  cursor: pointer;
  border-bottom: 2px solid transparent;
}

.ribbon-tab:hover:not(:disabled) {
  color: var(--ink);
}

.ribbon-tab[aria-selected="true"] {
  color: var(--accent-text);
  border-bottom-color: var(--accent-text);
}

.ribbon-tab:disabled {
  opacity: 0.4;
  cursor: default;
}

.ribbon-body {
  position: relative;
}

.ribbon-panel {
  display: flex;
  align-items: stretch;
  /* The band is a rounded card floating on the grey chrome field — the same
     treatment the compact toolbar gets, so the two modes read as one system
     rather than one pill and one full-bleed strip. White on grey, so it
     separates by SHADE and needs no border. */
  background: var(--surface);
  margin: var(--chrome-card-inset-y) var(--chrome-card-inset-x);
  border-radius: var(--chrome-card-radius);
  /* Vertical padding is free; horizontal padding is not — it comes straight out
     of the band's group budget (see --chrome-card-inset-x). The groups carry
     their own inline padding, so the panel needs only enough to keep the first
     control off the card's rounded edge. */
  padding: 6px clamp(2px, 0.35vw, 8px) 5px;
  /* No horizontal scrollbar ever (docs/64): a single no-wrap row whose
     overflowing groups collapse into the "⋯" menu instead. */
  overflow: hidden;
  min-width: 0;
  /* Every band is the same height, so switching tabs never moves the document.
     `.rgroup` already centres its rows and `align-items: stretch` above makes a
     one-row group fill the band, so its separator runs the full height — which
     is what makes Review and View read as the same object as Home rather than
     as a thinner strip. */
  min-height: var(--ribbon-band-h);
}

.ribbon-panel[hidden] {
  display: none;
}

/* With the captions gone (docs/123 §4) the vertical rule and the spacing are
   the ONLY thing that says where one group ends and the next begins, so both
   are ONLYOFFICE's: `.group { padding-left: 6px }` plus a `.separator` with
   `margin-left: 6px` — 12px of air around a one-pixel rule
   (`apps/common/main/resources/less/toolbar.less:498,545`). It was 2px a side
   around a `--line` hairline, which under a caption row was enough and without
   one is not: at 4px the Font group's last control sat against the Paragraph
   group's first. `--line-strong` rather than `--line` for the same reason. */
.rgroup {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Controls fill the band, caption pinned under them — Word's group box. */
  justify-content: space-between;
  gap: 3px;
  padding: 1px 8px 0;
  border-right: 1px solid var(--line-strong);
  flex: 0 0 auto;
}

.rgroup:last-child {
  border-right: none;
}

.rgroup-ctl {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 32px;
}

.rgroup-mini-divider {
  width: 1px;
  height: 22px;
  margin-inline: 2px;
  background: var(--line);
}

.table-ribbon .fmt {
  position: relative;
}

/* The badge's containing block is the BUTTON that carries it, wherever that
   button is. It was only ever made so for `.table-ribbon .fmt` above, so the
   one badge placed outside the Table band — a "G" on Review ▸ Grammar — had
   no positioned button to anchor to and resolved against `.ribbon-body`
   instead: `right: 2px; bottom: 1px` of the whole ribbon, painted as a stray
   "G" in the band's bottom-right corner, ~600px from the control it labelled.
   Stated on the badge's owner rather than on a band, so a badge cannot escape
   its control again by being used somewhere new.
   `ribbon-glyphs.spec.mjs` asserts nothing in a band paints outside the
   control that holds it. */
.fmt:has(> .table-command-badge) {
  position: relative;
}

.table-ribbon .table-danger:hover:not(:disabled) {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  color: var(--danger);
}

.table-command-badge {
  position: absolute;
  right: 2px;
  bottom: 1px;
  display: grid;
  min-width: 10px;
  height: 10px;
  place-items: center;
  border-radius: 3px;
  background: var(--surface);
  color: var(--muted);
  font-size: 7px;
  font-weight: 750;
  line-height: 1;
}

.table-context-group {
  min-width: 132px;
  max-width: 190px;
  justify-content: center;
  text-align: left;
}

.table-context-group .rgroup-hint {
  text-align: left;
}

/* Ribbon-group captions ARE drawn, which is Word's ribbon and is the standard
   the rest of this chrome already commits to: the tab strip is Word's tab set
   (File · Home · Insert · Layout · References · Review · View) with a
   contextual Table tab appended Word's way.

   docs/123 §4 removed them and followed ONLYOFFICE, whose band has no caption
   element at all. Seen on screen that left a band with no stated structure —
   the only grouping cue was a `--line-strong` hairline that is all but
   invisible in dark theme — so the band read as one undifferentiated run of
   icons. The owner looked at the result and rejected it: pick one standard.
   Word's, then, whole: captions under centred groups, and a separator that can
   actually be seen.

   The caption is also the group's accessible name (docs/105 P3) and the "⋯"
   overflow menu's section heading, so it was always in the DOM; this only stops
   hiding it. It costs ~15px of band height, which is what Word spends. */
.rgroup-label {
  max-width: 100%;
  overflow: hidden;
  color: var(--faint);
  font-size: var(--fs-ribbon-caption);
  font-weight: 500;
  line-height: 1;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* …except in the overflow menu, which is a menu, not a band: there the caption
   is the only thing that says which group a stack of icons came from. */
/* …and in the overflow menu it is a SECTION HEADING rather than a caption under
   a group, so it takes the heading treatment and sits above its controls. */
#ribbonOverflowMenu .rgroup-label {
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  text-align: left;
}

.rgroup-note {
  justify-content: center;
}

/* --- Home band composition (template.png / docs/64) ----------------------- */

/* General accessible-only utility for controls or explanatory text that must
   remain in the accessibility tree without occupying visual layout. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* Two stacked control rows inside one group (Font, Paragraph). */
.fmt-rows {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.fmt-row-line {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  min-height: 26px;
}

/* Keep the last Font control visibly inside its group instead of touching the
   Paragraph divider. The slightly tighter row gap matches the Vellum source.
   (Text color + highlight are now real dropdown swatch pickers — see the
   "Formatting toolbar UX v2" block at the end of this file.) */
[data-group="font"] .fmt-row-line {
  gap: 2px;
}

/* Clipboard group: a big Paste button beside stacked Cut/Copy text rows. */
.clipboard-ctl {
  align-items: stretch;
  gap: 2px;
}

/* Clipboard remains visibly discoverable even before a caret/range makes an
   action available; disabled controls are muted, not visually erased. */
.clipboard-ctl .fmt:disabled {
  opacity: 0.82;
  color: var(--faint);
}

.undo-ctl {
  align-items: center;
}

/* Two stacked rows of small buttons inside one group. This is ONLYOFFICE's
   `.elset` (`apps/common/main/resources/less/toolbar.less:530`): one row the
   height of a small button, the next one below it, and the pair filling the
   group. It was declared in the markup and styled nowhere — only `.undo-stack`
   carried the rules — so a second user of it (Clipboard, Editing) got a
   horizontal row. */
.stackrows {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
}

.undo-stack {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
}

.undo-stack .fmt {
  height: 30px;
}

.fmt.fmt-big {
  flex-direction: column;
  gap: 2px;
  width: auto;
  min-width: 44px;
  height: auto;
  padding: 5px 6px;
}

.editing-ctl {
  align-items: stretch;
  gap: 2px;
}

.fmt-big .ms {
  font-size: 22px;
}

.fmt-big-label {
  font-size: var(--fs-ribbon-caption);
  font-weight: 500;
  line-height: 1;
}

/* Styles is ONE control now (docs/114): a gallery of up to six preview cards. The
   all-styles `<select>` that used to sit above it, and the "▾ More styles" popover
   that listed every style again, are both gone — the full stylesheet lives in the
   command palette and in Paragraph properties, off the band. */
.rgroup-styles {
  min-width: 0;
  padding-inline: 3px;
}

.styles-ctl {
  align-items: center;
  min-width: 0;
}

/* ONE control, Google Docs' shape: a trigger labelled with the style the caret is
   in, opening a short list. It occupies the same 227px the select-above-a-strip
   composition and then the two-row card gallery each occupied, so the Home band's
   ~10px of slack at 1280px is untouched and no group is exiled into the "⋯" menu.

   Wide enough for the names the recommended set can produce ("Intense Quote" is
   the longest) without ellipsis; a custom in-use style with a very long name
   ellipsises here and the `title` carries the whole of it, which is the same trade
   `.font-family-trigger` makes and for the same reason. */
.styles-control {
  width: 227px;
  min-width: 0;
}

.styles-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 2px;
  width: 100%;
  height: var(--h-control);
  padding: 0 4px 0 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font-size: var(--fs-body);
  cursor: pointer;
  transition: border-color 0.11s, background 0.11s;
}

.styles-trigger:hover:not(:disabled) {
  border-color: var(--accent-line);
}

.styles-trigger:disabled {
  opacity: 0.5;
  cursor: default;
}

.styles-trigger:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

.styles-trigger[aria-expanded="true"] {
  border-color: var(--accent-text);
  background: var(--accent-soft);
}

.styles-trigger-label {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* No document open, or the caret is nowhere: the trigger still names a style so
   the control is not an empty box, but it reads as unset. */
.styles-trigger.is-placeholder .styles-trigger-label {
  color: var(--muted);
}

.styles-trigger-caret {
  flex: 0 0 auto;
  font-size: 18px;
  line-height: 1;
  color: var(--muted);
}

/* The list. Each row is drawn IN the style it applies — that is the whole reason
   this is a product popup and not a native `<select>`, whose rows an OS renders in
   the system font. Six rows at their own sizes, so the menu is short enough to
   never need a scrollbar at the cap. */
.styles-menu {
  min-width: 260px;
  max-width: 340px;
  padding: 4px;
}

/* The full style list is here, so it scrolls rather than growing the popover
   off the screen — a document can define forty paragraph styles and all of
   them are reachable. The suggested group sits above it and is short. */
.styles-menu-list {
  max-height: 320px;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

.style-option {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  /* Tall enough for a Title row at its preview size, and uniform so the list does
     not look ragged; the preview sizes are clamped in `applyStylePreview`. */
  min-height: 30px;
  padding: 4px 8px;
  text-align: left;
}

.style-option-name {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-weight: 450;
}

/* The tick uses the chrome's one check mark (`.menu-check`, opacity-toggled) so the
   Styles menu says "you are here" the same way every other menu in the product does. */
.style-option.is-selected .menu-check {
  opacity: 1;
}

.style-option.is-selected {
  color: var(--accent-text);
}

/* Fallback look for the well-known built-in styles. Each row is normally drawn in
   its own style from the engine's resolved `stylePreview` (family/size/weight/
   slant/underline/color, applied inline in `main.js`), which overrides these. They
   remain as a graceful degrade for when the preview is unavailable — before a
   document's engine is ready, say. */
.style-option-title .style-option-name { font-size: 19px; font-weight: 700; }
.style-option-subtitle .style-option-name { font-size: 14px; font-weight: 500; color: var(--muted); }
.style-option-heading-1 .style-option-name { font-size: 17px; font-weight: 700; }
.style-option-heading-2 .style-option-name { font-size: 15px; font-weight: 650; }
.style-option-heading-3 .style-option-name { font-size: 14px; font-weight: 600; }
.style-option-normal .style-option-name { font-size: 13px; font-weight: 450; }

/* Overflow "⋯" button + menu — collects groups that don't fit the width. */
.ribbon-overflow-btn {
  position: absolute;
  top: 6px;
  right: 8px;
  z-index: 3;
}

.ribbon-overflow-btn[hidden] {
  display: none;
}

.ribbon-overflow-menu {
  position: fixed;
  z-index: var(--z-selection-toolbar);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
  width: min(560px, calc(100vw - 12px));
  max-height: calc(100vh - 132px);
  overflow: auto;
  padding: 10px 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-popover);
  background: var(--surface);
  box-shadow: var(--shadow-popover);
  overscroll-behavior: contain;
}

.ribbon-overflow-menu[hidden] {
  display: none;
}

/* A relocated group becomes a full-width, internally wrapping command row.
   This avoids carrying a desktop-wide Font/Table group unchanged into a 360px
   popover. */
.ribbon-overflow-menu .rgroup {
  align-items: stretch;
  width: 100%;
  padding: 6px 0 8px;
  border-right: none;
  border-bottom: 1px solid var(--line);
  gap: 6px;
}

.ribbon-overflow-menu .rgroup:last-child {
  border-bottom: none;
}

.ribbon-overflow-menu .rgroup-label {
  order: -1;
  text-align: left;
  color: var(--muted);
  font-weight: 600;
}

.ribbon-overflow-menu .rgroup-ctl,
.ribbon-overflow-menu .fmt-row-line {
  flex-wrap: wrap;
}

/* In the 560px overflow popover the control gets the popover's width instead of
   the band's 227px. */
.ribbon-overflow-menu .styles-ctl,
.ribbon-overflow-menu .styles-control {
  width: 100%;
  max-width: none;
}

/* Delayed hover/focus tooltip for icon-only ribbon controls (docs/64 §3). */
.ribbon-tooltip {
  position: fixed;
  z-index: var(--z-tooltip);
  max-width: 240px;
  padding: 5px 8px;
  border-radius: 6px;
  background: var(--ink);
  color: var(--surface);
  font-size: var(--fs-small);
  line-height: 1.35;
  pointer-events: none;
  opacity: 0;
  transform: translateY(2px);
  transition: opacity 90ms ease, transform 90ms ease;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.24);
}

.ribbon-tooltip.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.ribbon-tooltip kbd {
  display: inline-block;
  margin-left: 4px;
  padding: 0 4px;
  border-radius: 3px;
  background: color-mix(in srgb, var(--surface) 22%, transparent);
  font-family: inherit;
  font-size: var(--fs-ribbon-caption);
}

/* A second line under the name, for a control that opts in with
   `data-tip-detail` because its name alone would mislead — Track changes,
   whose chord CYCLES three modes rather than toggling one, says so here.
   Word's tooltips carry exactly this: a bold name and chord, then a sentence. */
.ribbon-tooltip-detail {
  display: block;
  margin-top: 2px;
  opacity: 0.86;
}

/* Key tips (`key_tips.mjs`): Word's letter badges, in the tooltip's own
   inverted pair so they read in both themes without a new colour. Fixed to the
   viewport and pointer-transparent; centred on the point the module computes. */
.key-tips {
  position: fixed;
  inset: 0;
  z-index: var(--z-tooltip);
  pointer-events: none;
}

.key-tips[hidden] {
  display: none;
}

.key-tip {
  position: fixed;
  transform: translateX(-50%);
  min-width: 16px;
  padding: 1px 4px;
  border-radius: 3px;
  background: var(--ink);
  color: var(--surface);
  font-size: var(--fs-ribbon-caption);
  font-weight: 700;
  line-height: 1.3;
  text-align: center;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24);
}

.key-tip[hidden] {
  display: none;
}

/* A control that cannot run right now still shows its letter — the table of
   letters must not reshuffle as availability changes — but reads as unavailable,
   the way the control under it does. */
.key-tip.is-unavailable {
  opacity: 0.45;
}

.rgroup-hint {
  max-width: 190px;
  font-size: var(--fs-small);
  color: var(--faint);
  line-height: 1.4;
  text-align: center;
}

/* Select-based controls */
.ctl {
  display: inline-flex;
  align-items: center;
}

.ctl select,
.ctl > input[type="number"] {
  height: var(--h-control);
  padding: 0 26px 0 9px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong);
  background: var(--surface)
    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%236a7078'><path d='M7 10l5 5 5-5z'/></svg>")
    no-repeat right 5px center;
  background-size: 16px;
  color: var(--ink);
  appearance: none;
  cursor: pointer;
  max-width: 150px;
}

.ctl > input[type="number"] {
  padding-right: 4px;
  font: inherit;
  font-variant-numeric: tabular-nums;
  cursor: text;
}

.ctl select:hover,
.ctl > input[type="number"]:hover {
  border-color: var(--muted);
}

.ctl.size select,
.ctl.size > input {
  /* 80px, not 64: a decimal size clipped. Word allows half points and the engine
     stores eighths of a point, so "8.5" is four glyphs plus the spinner and the
     focus ring, and at 64px the last character was cut. The Home band's width
     budget has ~55px of slack at 1280px (`docs/63`), so this spends 16 of it —
     `ribbon-home` and `responsive-shell` are what prove the band still fits. */
  width: 80px;
  min-width: 80px;
}

.ctl select:disabled,
.ctl > input:disabled {
  opacity: 0.4;
  cursor: default;
}

.ctl.is-mixed {
  outline: 1px dashed var(--muted);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Text-color swatch */
.ctl.swatch {
  gap: 2px;
  height: var(--h-control);
  padding: 0 4px 0 7px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong);
}

.ctl.swatch span {
  font-weight: 700;
  font-size: 14px;
}

.ctl.swatch input[type="color"] {
  width: 20px;
  height: 20px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}

.ctl.swatch input[type="color"]::-webkit-color-swatch-wrapper {
  padding: 0;
}

.ctl.swatch input[type="color"]::-webkit-color-swatch {
  border: 1px solid var(--line-strong);
  border-radius: 4px;
}

/* Icon toggle buttons */
/* **`.fmt` has to answer `[hidden]`, because its own `display` outranks the user
   agent's.** `[hidden] { display: none }` is a UA rule, so any author `display` on
   the same element beats it — which means every `.fmt` button in this product that
   ships `hidden` was painted anyway.
   This was not theoretical and it was already known once: `.ribbon-overflow-btn`
   carries its own `[hidden]` rule further down, and the only reason it needs one is
   this. That rule is now redundant and is left where it is rather than removed in a
   change about something else; what is new here is that the NEXT `.fmt` button to
   ship `hidden` does not have to rediscover it. `session-rights.spec.mjs` is the
   guard — Manage access is absent, not greyed, for a participant whose grant
   withholds it, and a button the stylesheet painted anyway is that requirement
   silently broken.
   Before `.fmt` rather than after, so the cascade order is obvious at the point the
   `display` is set. */
.fmt[hidden] {
  display: none;
}

.fmt {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--h-control);
  height: var(--h-control);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  padding: 0;
  transition: background 0.11s, color 0.11s, border-color 0.11s;
}

.fmt .ms {
  font-size: var(--icon-control);
  display: block;
}

/* A glyph is an atom: it is never squeezed. Every `.ms` is `overflow: hidden`
   (fonts.css), and a flex item that clips its overflow has an automatic
   minimum width of ZERO — so wherever a button's content box was narrower than
   its icon, the icon shrank silently and lost its right-hand side: the
   zoom-presets caret drew 6 of its 16px (a dot), five split-button carets 12
   or 15 of 18. Refusing to shrink turns any future mismatch into visible
   overflow, which `ribbon-glyphs.spec.mjs` reports by name, instead of a
   clipped glyph nobody measures. */
.ms {
  flex-shrink: 0;
}

/* A ribbon button whose face is a short piece of TEXT rather than an icon.
   Material Symbols has no numeral ligature, so "100" as a `.ms` rendered the
   literal three characters and `.fmt`'s fixed square clipped it to "10" — a
   control that looks broken. The reference labels this one in words too:
   ONLYOFFICE's `slot-btn-zoom-100` is a `btn-slot text` with a "100%" caption
   (`ViewTab.js:79, 326-331`). Auto width, floored at the icon square so it
   still lines up with the icon buttons beside it. */
.fmt-num {
  width: auto;
  min-width: var(--h-control);
  padding: 0 6px;
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.format-glyph {
  display: block;
  font-size: 15px;
  line-height: 1;
}

.bold-glyph {
  font-weight: 700;
}

.italic-glyph {
  font-family: Georgia, serif;
  font-style: italic;
}

.underline-glyph {
  font-size: 14px;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.strike-glyph {
  font-size: 14px;
  text-decoration: line-through;
}

.fmt:hover:not(:disabled) {
  background: var(--bg-2);
}

.fmt[aria-pressed="true"] {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent-text);
}

.fmt[aria-pressed="mixed"] {
  border-color: var(--accent-line);
  color: var(--accent-text);
  background: linear-gradient(
    135deg,
    var(--accent-soft) 0 45%,
    transparent 45% 55%,
    var(--accent-soft) 55% 100%
  );
}

.fmt:disabled {
  opacity: 0.35;
  cursor: default;
}

/* Format painter: armed uses the shared accent-soft pressed state above; the
   locked (sticky) state fills the brush so "one-shot" vs "keep painting" reads
   at a glance, with a live pulse to signal it stays on. */
.fmt.is-sticky[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
  animation: format-painter-pulse 1.6s ease-in-out infinite;
}

@keyframes format-painter-pulse {
  0%,
  100% {
    box-shadow: 0 0 0 0 var(--accent-soft);
  }
  50% {
    box-shadow: 0 0 0 3px var(--accent-soft);
  }
}

@media (prefers-reduced-motion: reduce) {
  .fmt.is-sticky[aria-pressed="true"] {
    animation: none;
  }
}

/* Paintbrush cursor affordance while armed: shown over the document pages so the
   pointer signals it will paint formatting, not place a caret. The 24px brush
   points to its lower-left tip (hotspot 3 21); `copy` is the graceful fallback. */
body.is-format-painting .page-wrap,
body.is-format-painting .page-wrap * {
  cursor:
    url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M3 21c1.6.4 3.4-.2 4.2-1.6.5-.9.3-2-.4-2.7-.7-.7-1.8-.9-2.7-.4C2.7 17 2.4 19.2 3 21z" fill="%23e2622a" stroke="%23000" stroke-width="1.1" stroke-linejoin="round"/><path d="M8.5 15.5 18 6a2 2 0 0 0-2.8-2.8L6 12.6z" fill="%23fff" stroke="%23000" stroke-width="1.1" stroke-linejoin="round"/></svg>')
      3 21,
    copy;
}

/* ---- Viewport & pages ------------------------------------------------------ */
/* ---- Work area: nav rail · side panels · document viewport ----------------- */
.workarea {
  flex: 1;
  display: flex;
  min-height: 0;
  /* Canvas/grid: the rail + viewport sit flush under the ribbon's bottom
     hairline — no floating gap now that the ribbon is full-bleed. */
  margin-top: 0;
}

.rail {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  padding: 10px 6px;
  /* The rail is chrome, so it shares the one grey field rather than sitting on
     its own tint with an edge — the tint is what made it read as a separate bar
     glued to the header. */
  background: transparent;
  border-right: 0;
}

/* A text caption under the icon (doc 69 §1.2/§1.4.4): every rail destination
   in the reference has a visible label; OpenDoc's had icon+tooltip only.

   The caption was `--fs-micro` (9px, semibold) inside a 40x44 tile, which is
   below the size at which a word reads as a label rather than as texture — the
   rail looked cramped and the four destinations ran together. Word's navigation
   pane and Docs' outline pane both give a destination a full-size caption; this
   is a rail, so it takes the caption size the rest of the chrome uses for a
   caption (`--fs-caption`) in a tile wide enough to hold "Comments" without
   ellipsis, with real air between tiles. */
.rail-btn {
  /* Width comes from the longest caption, not from a number: "Comments" already
     ellipsised to "Comme…" at 56px in English, and German's "Kommentare" is
     longer still. `.rail` stretches its children, so every tile matches the
     widest one and the column stays a column. */
  width: auto;
  min-width: 52px;
  height: var(--h-rail-control);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 0 7px;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.rail-btn .ms {
  font-size: var(--icon-rail);
  /* One optical weight across the four destinations. Material Symbols is a
     variable font, so the glyphs differ in apparent weight unless `wght` is
     pinned: `auto_stories` and `rate_review` painted noticeably heavier than
     `toc` and `history` and the set did not read as one family. */
  font-variation-settings: "wght" 400, "opsz" 24, "GRAD" 0, "FILL" 0;
}

.rail-btn > span:not(.ms) {
  max-width: 100%;
  overflow: hidden;
  font-size: var(--fs-caption);
  font-weight: 550;
  line-height: 1.1;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* `--surface`, not `--bg-2`: the rail's own background IS `--bg-2` (see `.rail`
   above), so the old hover fill was the same colour as what it painted over, in
   every theme — a declaration that could never show. Only the icon recoloured,
   so the rail did not read as a set of buttons the way the rest of the shell's
   icon controls do (HF-096). `--surface` is the one token guaranteed to sit a
   step off `--bg-2` in both palettes. */
/* The rail sheds its captions when the review column takes the margin, for the
   same reason it sheds them on a phone: the room the document gets is whatever
   the rail and the column leave it.

   A caption-sized tile is 78px in English and wider in Russian or Brazilian
   Portuguese, and at 860px with the column in the margin those pixels come
   straight off the page — measured, the sheet starts at x=78 instead of x=40
   and the column then covers a third of it rather than a quarter. The icons
   keep their `aria-label`, which is what `editor-narrow-chrome.spec.mjs` asserts
   for the phone rung, so nothing becomes unreachable or unnameable.

   THE WIDTH CONDITION IS THE FIX (HF-265 D6). This rule carried none: its own
   comment justifies itself "at 860px", and it was matching at EVERY width, so
   opening Comments on a 1920x1080 screen with 1113px of spare room silently
   took the four rail captions away — measured, the rail went 78px -> 64px and
   `display` on `#railOutline`'s caption went `block` -> `none` at 1920. A
   justification that names a breakpoint and a selector that does not is the
   same defect as a dead token: the stylesheet states an intention it does not
   implement. 860px is the rung the column's own geometry already uses (see the
   `.review-sidebar` block below), so the two now move together; below 620px the
   captions are gone anyway, for the phone's own reason. */
@media (max-width: 860px) {
  :root:has(#viewport.has-review-sidebar:not(.review-sheet)) .rail-btn > span:not(.ms) {
    display: none;
  }
}

.rail-btn:hover:not(:disabled) {
  background: var(--surface);
  color: var(--ink);
}

.rail-btn[aria-pressed="true"] {
  background: var(--accent-soft);
  color: var(--accent-text);
}

.rail-btn:disabled {
  opacity: 0.4;
  cursor: default;
}

.side-panel {
  flex: 0 0 252px;
  width: 252px;
  min-width: 0;
  max-width: min(252px, calc(100vw - 52px));
  /* Inset floating panel: even breathing room on all sides (the rail stays
     flush under the ribbon; the panels float within the workarea). */
  margin: 8px;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-popover);
  box-shadow: var(--shadow-sm);
}

.side-panel[hidden] {
  display: none;
}

/* `top: 86px` put this panel's top edge inside the ribbon band: at z-index 65
   over the ribbon's 2, it painted across the ribbon's right-hand group and the
   "⋯" overflow button, so every command the overflow had just relocated became
   unclickable for as long as an object was selected (HF-084). It now starts
   below the chrome, from the same expression the 900px block already uses for
   the properties and glyph panels — one token, so the two cannot drift apart. */
/* An in-flow flex item in `.workarea`, like every other side panel — NOT a
   fixed overlay. As a fixed body child it painted over the canvas (the page
   never shifted aside for it) and over the status bar. Being a flex item also
   removes the need to anchor its top to `--chrome-bottom`: the row already
   starts below the chrome, in whichever chrome mode is showing. */
.object-inspector {
  flex: 0 0 280px;
  width: 280px;
  max-width: min(280px, calc(100vw - 52px));
  z-index: var(--z-inspector);
}

.property-grid-2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}

/* Symbols and emoji are persistent document-side tools, not blocking dialogs.
   Keep the page visible while repeated glyph insertions happen. */
.glyph-panel {
  /* Contextual tools belong in the right-hand inspector column, never between
     the navigation rail and the document canvas. */
  order: 3;
  flex-basis: 360px;
  width: 360px;
  max-width: min(360px, calc(100vw - 52px));
}

/* The legacy left-side review inventory is retained only as compatibility
   wiring. The active review surface is the dedicated right column below. */
#reviewPanel { display: none !important; }

.review-popover {
  position: fixed;
  z-index: var(--z-review-popover);
  width: min(320px, calc(100vw - 24px));
  padding: 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-popover);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-lg);
}
.review-popover-head { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; }
.review-popover-meta { color: var(--muted); font-size: 11px; }
.review-popover-body { margin: 8px 0; font-size: var(--fs-small); line-height: 1.45; white-space: pre-wrap; }
.review-popover-actions { display: flex; flex-wrap: wrap; gap: 6px; }
/* Inline accept/reject card (review UX v2 Q2): a compact popover surfaced on
   hover/press of a tracked-change marker on the canvas — author, one-line
   summary, and ✔ Accept / ✗ Reject. Reuses the popover surface tokens so it
   matches the rest of the editor chrome in light and dark themes. */
.review-inline-card {
  position: fixed;
  z-index: var(--z-review-card);
  width: max-content;
  max-width: min(340px, calc(100vw - 24px));
  padding: 10px 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-popover);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-lg);
  outline: none;
}
.review-inline-card:focus-visible { box-shadow: var(--shadow-lg), 0 0 0 2px var(--accent-text); }
.review-inline-head { display: flex; align-items: center; gap: 8px; }
.review-inline-dot { width: 10px; height: 10px; flex: 0 0 10px; border-radius: 50%; background: var(--accent-strong); }
.review-inline-who { display: grid; gap: 1px; min-width: 0; }
.review-inline-who strong { font-size: var(--fs-small); font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.review-inline-who small { color: var(--muted); font-size: var(--fs-caption); }
.review-inline-body {
  margin: 7px 0 9px;
  font-size: var(--fs-small);
  line-height: 1.45;
  overflow-wrap: anywhere;
}
.review-inline-bar { display: flex; gap: 6px; }
.review-inline-bar button {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 4px 11px 4px 9px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-caption);
  font-weight: 600;
  cursor: pointer;
}
.review-inline-bar button .ms { font-size: 16px; }
.review-inline-bar button:hover { background: var(--review-insert-soft); border-color: var(--review-insert-line); color: var(--review-insert); }
.review-inline-bar button:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 1px; }
.review-inline-bar button.danger:hover { background: var(--review-delete-soft); border-color: var(--review-delete-line); color: var(--review-delete); }
/* A real segmented control: a drawn track holding three segments, so the two
   that are not selected still read as parts of one switch rather than as loose
   words in the status bar. The track was `--bg-2` — the footer's own colour, in
   both themes — which is why only the selected pill was visible. */
.review-mode-segmented { display: inline-flex; gap: 0; padding: 1px; border: 1px solid var(--line-strong); border-radius: 999px; background: var(--surface); }
.review-mode-seg { display: inline-flex; align-items: center; gap: 4px; border: 1px solid transparent; border-radius: 999px; padding: 4px 9px; background: transparent; color: var(--muted); font: inherit; font-size: var(--fs-ribbon-caption); font-weight: 600; cursor: pointer; white-space: nowrap; }
.review-mode-seg .ms { font-size: var(--fs-medium); }
.review-mode-seg[aria-pressed="true"] { color: var(--review-insert); border-color: var(--review-insert-line); background: var(--review-insert-soft); }
.review-mode-seg[data-review-mode="viewing"][aria-pressed="true"] { color: var(--ink); border-color: var(--accent-text); background: var(--accent-soft); }
.review-mode-seg:disabled { opacity: 0.45; cursor: default; }
.review-mode-seg:not(:disabled):not([aria-pressed="true"]):hover { background: var(--accent-soft); color: var(--ink); }
.viewing-banner { border-color: color-mix(in srgb, var(--accent-text) 42%, var(--line)); background: color-mix(in srgb, var(--accent-text) 12%, var(--surface)); }
.viewing-banner .ms { color: var(--accent-text); }
.suggesting-banner {
  min-height: 32px;
  margin: 6px 8px 0;
  padding: 0 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 1px solid color-mix(in srgb, var(--warning-line) 42%, var(--line));
  border-radius: 8px;
  background: var(--warning-soft);
  color: var(--ink);
  font-size: var(--fs-small);
}
.suggesting-banner[hidden] { display: none; }
.suggesting-banner .ms { color: var(--warning); font-size: 17px; }
.suggesting-banner button {
  border: 0;
  padding: 3px 6px;
  border-radius: 5px;
  background: transparent;
  color: var(--accent-strong);
  font: inherit;
  font-weight: 650;
  cursor: pointer;
}
.suggesting-banner button:hover { background: var(--accent-soft); }

/* The version-preview bar. Same shape and rhythm as the two review banners it
   sits with — "the document on screen is not the one you are editing" is the
   same class of fact as "your edits are being tracked" — and it carries the
   ACCENT rather than the warning palette for the recovery bar's reason: looking
   at an earlier version is not a problem, it is a thing the reader asked for. */
.version-preview-banner {
  border-color: color-mix(in srgb, var(--accent) 42%, var(--line));
  background: var(--accent-soft);
}
.version-preview-banner .ms { color: var(--accent-text); }

/* Crash-recovery offer (docs/112 §4.6, HF-011). Shares the banner's placement
   and rhythm so the chrome stays one system, but carries the accent rather than
   the warning palette: recovered work is good news, and the warning colours are
   already spoken for by "your edits are being tracked". */
.draft-recovery {
  margin: 6px 8px 0;
  padding: 8px 10px;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  border: 1px solid color-mix(in srgb, var(--accent) 38%, var(--line));
  border-radius: 8px;
  background: var(--accent-soft);
  color: var(--ink);
  font-size: var(--fs-small);
}
.draft-recovery[hidden] { display: none; }
.draft-recovery-icon { color: var(--accent-strong); font-size: 18px; }
.draft-recovery-body { flex: 1 1 auto; min-width: 0; }
.draft-recovery-title { margin: 0; font-weight: 650; }
.draft-recovery-list {
  margin: 4px 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.draft-recovery-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  min-width: 0;
}
.draft-recovery-name {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 40ch;
}
.draft-recovery-meta { color: var(--muted); }
.draft-recovery button {
  border: 0;
  padding: 3px 8px;
  border-radius: 5px;
  background: transparent;
  color: var(--accent-strong);
  font: inherit;
  font-weight: 650;
  cursor: pointer;
}
.draft-recovery button:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
.draft-recovery-dismiss { color: var(--muted); flex: 0 0 auto; }
.draft-recovery-dismiss .ms { font-size: 17px; }

/* Autosave's footer indicator. Muted by default — it is reassurance — and it
   takes the danger colour only in the one state the user may need to act on. */
.draft-status {
  color: var(--muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  /* Yields to the engine's message as the counts do (`.status` takes its
     width first), rather than pushing it off the end of the half. */
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.draft-status[hidden] { display: none; }
.draft-status[data-draft-state="unavailable"] { color: var(--danger); font-weight: 600; }

/* The reader's own access, in the status bar — `access_badge.mjs`.
   Existing tokens only: `--muted` for the whole thing and `--ink` for the level,
   which is the same two-weight treatment `.draft-status` and `.stat-pages`
   already use for "label plus value". No new token, no new colour (`SKILL` §11:
   the token set is deliberate; propose visual changes, do not make them).
   It is `inline-flex` with a glyph because the footer's other indicators are
   words only, and a reader scanning for "what am I allowed to do" needs one
   fixed thing to find. The glyph is `aria-hidden`; the sentence is on the
   container's `title`/`aria-label`. */
.access-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--muted);
  white-space: nowrap;
  /* **It shrinks rather than pushes**, and this is the line that matters: at
     320px the footer squeezed this box to 28px against a 68px content width, and
     a box that will not shrink turns that into a SIDEWAYS SCROLL of the whole
     chrome — which `phone-no-horizontal-scroll.spec.mjs` caught, by name, on the
     first run. The no-horizontal-scroll rule is the one the ribbon budget turns
     on too (`SKILL` §11): what breaks first is never an overflow menu, it is a
     scrollbar. */
  flex: 0 1 auto;
  min-width: 0;
}

.access-badge .ms {
  font-size: 15px;
  /* The glyph is the one part that may not shrink — a half-drawn icon is worse
     than none — so it is excluded from the squeeze and the text absorbs it. */
  flex: 0 0 auto;
}

.access-badge-level {
  color: var(--ink);
  /* Truncates rather than overflowing. The whole statement is on the container's
     `title`/`aria-label`, so a clipped label costs a sighted reader the end of a
     word and costs a screen reader nothing. */
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

/* The SOURCE is not painted in the status bar at all, at any width — the level
   is the answer and the source is the attribution, and the attribution is the
   container's `title` and `aria-label`, which carry the whole statement.
   It used to be painted above 900px, and at 1280px "Full access on this
   device, where you are the only authority" took 357px of a row it shares
   with the engine's own messages, which were left 0-106px and read "Cou…",
   "Fetc…", "Draf…". Word and Google Docs both state status in a word or two
   and explain it on hover; a sentence that never changes for a standalone
   reader is the worst possible use of the one row that carries every
   transient message. `footer-status.spec.mjs` holds the message's room. */
.access-badge-source {
  display: none;
}

/* At phone width the glyph goes too, and the level keeps every pixel there is.
   A 15px icon plus its gap is a fifth of what this box gets at 320px, and the
   WORD is the thing a reader needs — "Read only" clipped to "Read on…" still
   answers the question, and a lock glyph on its own does not say whether it
   means read-only, comments-only or suggest-only. */
@media (max-width: 620px) {
  .access-badge .ms {
    display: none;
  }
}

/* Manage access: one row per participant, a name and a role picker.
   The rows are generated (`session_rights.mjs`), so unlike every other dialog
   body in this file the layout is here rather than in the markup. Built from
   `.settings-section`'s own spacing so it sits in the same rhythm as the
   authored sections around it. */
.rights-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.rights-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-width: 0;
}

.rights-who {
  color: var(--ink);
  font-size: var(--fs-small);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rights-role {
  flex: 0 0 auto;
}

/* One scroll owner (Google Docs model): the comment column is an
   absolutely-positioned layer INSIDE `.viewport`'s scroll context, not a
   sibling `overflow:auto` container. It never scrolls on its own, so cards —
   positioned in document-scroll coordinates — ride natively with their anchored
   text (no JS scroll-sync, no card drift under momentum) and the single
   scrollbar sits at `.viewport`'s far-right edge, past the comments. */
.review-sidebar {
  position: absolute;
  top: 0;
  right: 12px;
  width: 340px;
  min-width: 340px;
  color: var(--ink);
  background: transparent;
}
.review-sidebar[hidden] { display: none; }
/* Fixed workflow chrome above the (virtualized, document-scroll-positioned) card
   area (REVIEW-GAP-018/019). The card body rides the canvas scroll, so the header
   is `sticky` to stay visible; `.review-sidebar-body`'s negative margin (set from
   JS to the header's height) cancels the header's flow offset so cards stay
   pixel-aligned to their canvas anchors. */
.review-sidebar-header {
  position: sticky;
  top: 8px;
  z-index: 6;
  margin: 8px 0 6px;
  /* The PADDING convention, not the geometry one (HF-265). This column is
     absolutely positioned and rides the document scroll so each card stays
     pinned to its anchor — that is the model, and it is deliberately outside
     the `.panel-head`/`.panel-body` shell. What it shares with the shell is the
     inset: this header and `.review-margin-card` below both said `8px 10px`,
     which was already self-consistent and was two more raw pixel values in a
     family that now has a token pair. */
  padding: var(--pnl-pad-y) var(--pnl-pad-x);
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
  display: grid;
  gap: 8px;
}
.review-sidebar-headrow { display: flex; align-items: center; gap: 8px; }
.review-sidebar-title { flex: 1; font-size: var(--fs-small); font-weight: 600; color: var(--ink); }
.review-sidebar-nav { display: flex; gap: 4px; }
.review-nav-button {
  width: 26px; height: 26px; padding: 0;
  display: inline-grid; place-items: center;
  border: 1px solid var(--line); border-radius: 6px;
  background: transparent; color: var(--ink); cursor: pointer;
}
.review-nav-button:hover:not(:disabled) { background: var(--accent-soft); border-color: var(--accent-text); }
.review-nav-button:disabled { opacity: 0.4; cursor: default; }
.review-nav-button .ms { font-size: 18px; }
.review-sidebar-controlrow { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.review-sidebar-header .review-bulk-actions { padding: 0; margin: 0; }
.review-sidebar-header .review-bulk-actions[hidden] { display: none; }
.review-sidebar-header .review-card-action { margin-top: 0; }
.review-sidebar-body {
  position: relative;
  width: 100%;
}
.review-sidebar-empty {
  margin-top: 12px;
  padding: 24px 16px;
  border-radius: 10px;
  color: var(--muted);
  background: var(--surface);
  text-align: center;
  font-size: var(--fs-small);
  line-height: 1.5;
  box-shadow: var(--shadow-sm);
}
/* The margin comment affordance — Google Docs' Add-comment button, in the
   right margin, on the line the selection starts on. Positioned by
   `review_chrome.mjs` in the same scrolled content the cards live in, so it
   rides the document scroll rather than being re-placed per frame. The 32px box
   is `COMMENT_AFFORDANCE_SIZE` in `review_layout.mjs`, which is what decides
   whether the margin is wide enough to hold it; `review_layout.test.mjs` reads
   both so they cannot drift. No z-index, for the same reason `.review-sidebar`
   has none: it is a later sibling of the page stack and paints above it. */
/* Google Docs' margin button, and Docs draws it ROUND. As a square with a
   `--radius` corner, a hairline edge and an 18px glyph it read as an
   unexplained "+" floating beside the page; round, edged and in the accent it
   reads as the one affordance it is. Its `title` says what it does, and says
   what is missing when it is disabled (REVIEW_SURFACE's `reasonKey` in main.js).
   The BOX stays 32px: `review_layout.mjs` measures the margin against that
   number and `review_layout.test.mjs` holds the two together. */
.review-margin-add {
  position: absolute;
  width: 32px;
  height: 32px;
  padding: 0;
  display: grid;
  place-items: center;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--accent-text);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.review-margin-add[hidden] { display: none; }
/* Dimmed, not gone, while nothing is selected: `review.comment` anchors to a
   range, and a control that vanishes teaches nothing about how to get it back.
   Its accessible name says what the missing precondition is. */
.review-margin-add:disabled { opacity: 0.4; cursor: default; }
.review-margin-add:hover:not(:disabled) { background: var(--accent-soft); border-color: var(--accent-text); color: var(--accent-text); }
.review-margin-add:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }
.review-margin-add .ms { font-size: 20px; }
.review-margin-card {
  position: absolute;
  left: 0;
  right: 0;
  width: auto;
  box-sizing: border-box;
  /* Same inset as `.review-sidebar-header`, from the same pair — the two are
     read as one column and were already equal at `8px 10px`. */
  padding: var(--pnl-pad-y) var(--pnl-pad-x);
  border: 0;
  border-radius: 8px;
  background: var(--bg-2);
  color: var(--ink);
  box-shadow: 0 1px 3px rgba(60, 64, 67, 0.2), 0 2px 6px rgba(60, 64, 67, 0.08);
  cursor: pointer;
  transition: top 150ms ease, box-shadow 150ms ease, background 150ms ease;
}
.review-margin-card.expanded,
.review-margin-card.review-margin-composer {
  padding: 10px 12px;
  background: var(--surface);
  box-shadow: 0 1px 3px rgba(60, 64, 67, 0.3), 0 4px 8px 3px rgba(60, 64, 67, 0.15);
}
.review-margin-card:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }
.review-margin-card.resolved { opacity: 0.72; }
.review-margin-card-head { display: flex; align-items: flex-start; gap: 9px; }
/* Per-author avatar chip (docs/81 REVIEW-GAP-015): filled with the author's
 * stable palette color (`--review-author-color`, set per card) with white text
 * for legible contrast in light and dark themes. Falls back to the theme accent
 * when a card sets no author color. */
.review-margin-avatar { --review-author-color: var(--accent-strong); width: 26px; height: 26px; flex: 0 0 26px; display: grid; place-items: center; border-radius: 50%; background: var(--review-author-color); color: var(--accent-ink); font-size: 11px; font-weight: 700; }
.review-margin-title { min-width: 0; display: grid; gap: 1px; }
.review-margin-title strong { font-size: var(--fs-small); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.review-margin-title small { color: var(--muted); font-size: var(--fs-caption); text-transform: capitalize; }
.review-margin-card p { margin: 8px 0; font-size: var(--fs-small); line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; }
.review-margin-revision p { padding-left: 8px; border-left: 3px solid var(--accent-text); }
.review-margin-insertion p { border-left-color: var(--review-insert-line); color: var(--review-insert); }
.review-margin-deletion p { border-left-color: var(--review-delete-line); color: var(--review-delete); text-decoration: line-through; }
.review-margin-replacement p { border-left-color: var(--review-replace-line); }
.review-margin-formatting p { border-left-color: var(--review-format-line); }
.review-margin-move p { border-left-color: var(--review-move); }
.review-move-path {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 7px;
  margin: 4px 0 9px 34px;
  color: var(--muted);
}
.review-move-end {
  display: grid;
  gap: 1px;
  min-width: 0;
  text-align: left;
  padding: 3px 6px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--muted);
  font: inherit;
  cursor: pointer;
}
.review-move-end:hover { background: var(--bg-2); }
.review-move-end:focus-visible {
  outline: none;
  border-color: var(--accent-text);
  box-shadow: 0 0 0 2px var(--accent-soft);
}
.review-move-end[disabled] { cursor: default; opacity: 0.6; }
.review-move-end b { color: var(--ink); font-size: var(--fs-caption); }
.review-move-end span { font-size: var(--fs-micro); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.review-move-path > .ms { color: var(--review-move); font-size: var(--fs-medium); }
.review-margin-revision:has(.review-margin-title small) { border-color: var(--line-strong); }
.review-margin-card-actions { display: flex; gap: 5px; flex-wrap: wrap; }
.review-margin-card:not(.expanded) .review-margin-card-actions,
.review-margin-card:not(.expanded) .review-margin-replies,
.review-margin-card:not(.expanded) .review-reply-composer { display: none; }
.review-margin-replies { display: grid; gap: 8px; margin: 10px -2px 9px 34px; padding-left: 10px; border-left: 2px solid var(--line); }
.review-margin-reply { display: grid; gap: 4px; }
.review-margin-reply.resolved { opacity: 0.65; }
.review-margin-reply-head { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; }
.review-margin-reply-head strong { font-size: 11px; }
.review-margin-reply-head small { color: var(--muted); font-size: var(--fs-micro); }
.review-margin-reply p { margin: 0; padding: 0; border: 0; color: var(--ink); font-size: 11px; text-decoration: none; }
.review-margin-action { border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px; background: transparent; color: var(--ink); font: inherit; font-size: var(--fs-caption); cursor: pointer; }
.review-margin-action:hover { background: var(--accent-soft); border-color: var(--accent-text); }
.review-margin-action.danger { color: var(--danger); }
/* REVIEW-GAP-011: per-reply edit/delete controls; REVIEW-GAP-012: related-comment list. */
.review-margin-reply-actions { display: flex; gap: 6px; margin-top: 2px; }
.review-margin-reply-edit { width: 100%; box-sizing: border-box; border: 1px solid var(--line); border-radius: 6px; padding: 4px 6px; font: inherit; font-size: 11px; color: var(--ink); background: var(--surface, transparent); }
.review-margin-revision-comments { display: grid; gap: 6px; margin: 9px -2px 2px 34px; padding-left: 10px; border-left: 2px solid var(--line); }
.review-margin-revision-comments > small { color: var(--muted); font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.04em; }
.review-margin-revision-comment { display: grid; gap: 3px; }
.review-margin-revision-comment strong { font-size: 11px; }
.review-margin-revision-comment p { margin: 0; color: var(--ink); font-size: 11px; }
.review-margin-icon-action {
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  display: inline-grid;
  place-items: center;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.review-margin-icon-action:hover { background: var(--bg-2); color: var(--ink); }
.review-margin-title { flex: 1; }
.review-margin-body {
  display: block;
  max-height: 60px;
  overflow: hidden;
}
.review-margin-card.expanded .review-margin-body { max-height: none; }
.review-reply-composer,
.review-margin-composer {
  display: grid;
  gap: 8px;
}
.review-reply-composer textarea,
.review-margin-composer textarea {
  width: 100%;
  min-height: 34px;
  box-sizing: border-box;
  padding: 8px 12px;
  border: 1px solid var(--line-strong);
  border-radius: 18px;
  outline: none;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-small);
  line-height: 1.45;
  /* Height is driven by JS auto-grow (Q5); no manual resize handle, no
     inner scrollbar until the cap is reached. */
  resize: none;
  overflow: hidden;
}
.review-reply-composer textarea:focus,
.review-margin-composer textarea:focus { border-color: var(--accent-text); box-shadow: 0 0 0 2px var(--accent-soft); }
.review-margin-composer textarea { min-height: 60px; }
.review-composer-actions {
  display: flex;
  justify-content: flex-end;
  gap: 6px;
}

/* Narrow-window rungs for the comment column. The COLUMN's own geometry is
   overridden here, next to its base rule; the page-stack gutter that pairs with
   each rung lives beside `.viewport.has-review-sidebar .pages` further down,
   because that rule is declared after this point in the file and a media query
   adds no specificity — an override written here would simply lose on source
   order. That is not hypothetical: the 860px rung below shipped with its
   `padding-right: 316px` written in this block, where it never once applied. */
@media (max-width: 860px) {
  .review-sidebar {
    right: 8px;
    width: 300px;
    min-width: 300px;
  }
}

/* Below tablet-portrait the reserved-gutter model stops working: there is no
   window left to reserve from. At 390px the column and its page gutter left the
   document about 74px of usable width and pushed the sheet off the right edge —
   the flagship comments surface making the document it annotates unreadable
   (HF-088). Floating the column over the page fixed the overflow but not the
   occlusion: a 320px card stack still covered the text it points at. So below
   this width the column is not a column — it is a bottom sheet, and the rules
   for it live beside the `has-review-sidebar` gutter further down, because they
   have to beat it on source order. The width itself is duplicated in main.js as
   `REVIEW_SHEET_MAX_WIDTH`: the CARD LAYOUT changes shape here too (cards become
   a list in the sheet's own scroll rather than being pinned to canvas anchors),
   and a media query cannot tell main.js which shape it picked. */
@media (max-width: 700px) {
  .review-sidebar {
    right: 8px;
    min-width: 0;
  }
}
/* ---- The side-panel shell -------------------------------------------------
 *
 * One head, one body, one token pair — the mechanism the dialogs already prove
 * works here. Before this, `.side-panel` was a width plus a margin plus a
 * border and nothing else: the head and the body each stated their own padding,
 * in raw pixels, and five panels restated it again. Nine surfaces, no shell.
 *
 * `min-height` rather than `height`, so a head carrying a title AND a subtitle
 * (the two glyph panels and the two properties panels) grows instead of needing
 * a per-panel override to un-fix the height. That is what removed four of the
 * overrides rather than re-tuning them. */
.panel-head {
  display: flex;
  min-width: 0;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--h-rail-control);
  padding: var(--pnl-pad-y) var(--pnl-pad-x);
  border-bottom: 1px solid var(--line);
}

.panel-title {
  font-weight: 650;
  font-size: var(--fs-medium);
}

.panel-close {
  width: var(--h-control);
  height: var(--h-control);
  display: grid;
  place-items: center;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.panel-close .ms {
  font-size: var(--icon-compact);
}

.panel-close:hover {
  background: var(--bg-2);
  color: var(--ink);
}

/* The body takes the SAME inset as the head. That is the whole of HF-265 D2:
 * a head at 14px over a body at 6px is a panel whose title does not sit over
 * its content, and it was true of all nine panels.
 *
 * A row inside the body may still carry its own inset — the outline tree's
 * 18px disclosure gutter, a version row's hit target — and that is the row's
 * business, exactly as a `.dialog-group` fieldset's inset is inside a dialog
 * body. What this rule fixes is the SHELL, which is what the eye reads as
 * alignment. `e2e/panel-alignment.spec.mjs` asserts head and body resolve to
 * one content edge and that the title's text lands on it. */
.panel-body {
  flex: 1;
  min-width: 0;
  overflow: auto;
  padding: var(--pnl-pad-y) var(--pnl-pad-x);
  overscroll-behavior: contain;
}

.side-panel.table-properties-panel,
.side-panel.properties-panel {
  flex-basis: 320px;
  width: 320px;
  max-width: min(320px, calc(100vw - 52px));
}

/* Two lines of heading, no padding of its own: the shell's `min-height` and
 * `var(--pnl-pad-*)` both apply, which is what the `height: auto; min-height:
 * 58px; padding: 9px 8px 9px 12px` this replaces was working around. */
.table-properties-head,
.properties-panel-head {
  min-height: 58px;
}

.table-panel-heading,
.properties-panel-heading {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 9px;
}

.table-panel-heading > span:last-child,
.properties-panel-heading > span:last-child {
  display: flex;
  min-width: 0;
  flex-direction: column;
}

/* The 30px `--accent-soft` tile that used to lead this heading is GONE, and
 * that is a deliberate visual change rather than a side effect (HF-265 D1).
 *
 * Measured at 1280x720: it put the panel's title text at x=1004 inside a body
 * whose intro started at 967, its fieldset legends at 986 and its labels at
 * 981 — FOUR left edges in a 320px column, and the tile was the whole of the
 * 37px step. Nothing else in the product does this: of the 23 dialogs exactly
 * ONE carries a `.dialog-heading-icon`, the confirmation, and a confirmation
 * has no `.dialog-body` for its heading to sit over. Word's Format panes,
 * Google Docs' sidebars and ONLYOFFICE's right-hand panel all title a side
 * panel with plain text at the panel's own inset, for this reason.
 *
 * The tile was identity the panel did not need — it is opened from a named
 * control, it names itself in its title, and it states its subject in the
 * `small` beneath. What it cost was the one alignment a reader actually sees. */

.table-panel-heading small,
.properties-panel-heading small {
  overflow: hidden;
  color: var(--muted);
  font-size: var(--fs-caption);
  font-variant-numeric: tabular-nums;
  font-weight: 450;
  text-overflow: ellipsis;
  white-space: nowrap;
}


.table-properties-intro,
.properties-panel-intro {
  margin: 0 0 15px;
  color: var(--muted);
  font-size: var(--fs-small);
  line-height: 1.45;
}

.properties-panel-grid,
.table-properties-grid {
  display: grid;
  gap: 14px;
}

.property-section,
.table-property-section {
  display: grid;
  gap: 11px;
  padding: 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
}

.property-section legend,
.table-property-section legend {
  margin: 0;
  padding: 0 5px;
}

.property-color-row,
.property-border-row {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.paragraph-align {
  width: 100%;
}

.paragraph-align button {
  flex: 1;
}

.mixed-note {
  color: var(--muted);
  font-size: var(--fs-caption);
  font-style: italic;
}

/* Outline: a clickable heading tree, indented by level. */
.outline-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 5px 8px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.outline-item:hover {
  background: var(--accent-soft);
  color: var(--accent-text);
}

.outline-item.is-active {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  color: var(--accent-text);
  font-weight: 650;
}

.outline-item.lvl-1 {
  font-weight: 600;
}

.outline-item.lvl-2 {
  padding-left: 20px;
}

.outline-item.lvl-3 {
  padding-left: 32px;
}

.outline-item.lvl-4,
.outline-item.lvl-5,
.outline-item.lvl-6 {
  padding-left: 44px;
}

/* Depth reads as indent + quieter ink — but the quieter ink must not outrank
   the hover/current-location accent. `:hover` and `.is-active` above are both
   (0,2,0), and so is `.outline-item.lvl-3`; declared later, the depth colour
   simply won on source order and left an H3 row grey while it was the caret's
   own heading, so the cue got WEAKER the deeper you went (HF-095). Excluding
   the two states here restores the intended order without depending on where in
   the file these rules happen to sit. */
.outline-item.lvl-3:not(:hover):not(.is-active),
.outline-item.lvl-4:not(:hover):not(.is-active),
.outline-item.lvl-5:not(:hover):not(.is-active),
.outline-item.lvl-6:not(:hover):not(.is-active) {
  color: var(--muted);
}

/* ---- Folding (ADR-049) ------------------------------------------------------
   The outline is a `role="tree"` whose rows are a disclosure plus a label, so a
   row is a flex line and the label keeps every rule `.outline-item` already had
   — no new colour, radius or type size, because docs/63's tokens are deliberate
   and this feature has no business proposing a visual change. */
.outline-tree {
  display: block;
}

.outline-row {
  display: flex;
  align-items: center;
  gap: 2px;
}

/* The disclosure is a REAL focusable button (ONLYOFFICE's is a bare `div` with
   no role and no tabindex, which is the mistake to avoid). It is 18px square so
   the row's height is still the label's, and a leaf renders the same box as an
   inert spacer so text at one level always starts at the same x. */
.outline-twisty {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  border: none;
  border-radius: 4px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.outline-twisty .ms {
  font-size: 16px;
  line-height: 1;
}

.outline-twisty:hover {
  background: var(--accent-soft);
  color: var(--accent-text);
}

/* Withheld, not dead: `aria-disabled` keeps the reason reachable where
   `disabled` would hide the title from assistive technology. */
.outline-twisty[aria-disabled="true"] {
  cursor: default;
  opacity: 0.45;
}

.outline-twisty[aria-hidden="true"] {
  cursor: default;
}

/* The row's label now sits beside a disclosure, so its own left padding is the
   indent only — the twisty supplies the gutter. */
.outline-row .outline-item {
  flex: 1 1 auto;
  width: auto;
}

/* The in-body chevron at the caret's heading — Word's affordance, which
   ONLYOFFICE does not have at all. Positioned by the chrome against the caret
   rectangle, so it needs an absolute box in the page's own coordinate space. */
.overlay .fold-body-chevron {
  position: absolute;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  /* The margin offset is applied in `fold_chrome.mjs` as clamped
     arithmetic, because a negative margin here cannot be clamped and paints
     outside the window when the page margin is narrower than the chevron. */
  padding: 0;
  border: none;
  border-radius: 4px;
  background: transparent;
  /* THE PAPER LAYER, not the chrome palette. This chevron is painted over the
     page raster, and the sheet is white in BOTH themes — so `var(--muted)` put
     a dark-theme light grey onto white paper at 2.24:1, which the white-label
     contrast sweep caught. Same rule, and the same reason, as the caret and the
     review markers above: anything over the raster has a fixed contrast partner
     and must not follow the theme. */
  color: var(--paper-ink);
  cursor: pointer;
  /* The overlay is `pointer-events: none` so a click reaches the page canvas
     and places the caret. A CONTROL in that overlay has to opt back in, the way
     `.review-revision-marker` below does — without this the chevron paints,
     shows a pointer cursor, and cannot be clicked at all. */
  pointer-events: auto;
}

.overlay .fold-body-chevron .ms {
  font-size: 16px;
  line-height: 1;
}

/* Hover stays in the paper layer too: `--accent-soft`/`--accent-text` are a
   CHROME pair and would put the dark theme's pale accent ink on white paper.
   A 10% wash of the paper's own ink instead — no new token and no new hue, so
   `docs/63`'s palette is untouched — and the glyph keeps its full-contrast
   colour rather than trading contrast for emphasis. */
.overlay .fold-body-chevron:hover {
  background: color-mix(in srgb, var(--paper-ink) 10%, var(--paper));
}

.outline-empty {
  padding: 14px 10px;
  color: var(--faint);
  font-size: var(--fs-small);
  line-height: 1.5;
}

/* ---- Version history (docs/139, docs/140; HF-068 / 105 OO-004) -------------
   Reuses `.side-panel`, `.panel-head`, `.panel-body`, `.dialog-button` and
   `.outline-empty` wholesale: docs/63's design tokens are deliberate and this
   panel introduces NO new colour, radius or type size. It is wider than the
   252px navigators for the reason the properties panels are — its rows carry two
   lines and its actions are full-width buttons — and that width comes from the
   existing `.side-panel.properties-panel` value rather than a new one. */
.side-panel.version-panel {
  flex-basis: 320px;
  width: 320px;
  max-width: min(320px, calc(100vw - 52px));
}

/* A BAND of the panel shell, so it takes the shell's inset like the head and
   the body above and below it. At 12px it put the "Only named versions"
   checkbox 4px to the left of the title it sits under, which is the same
   defect the head and body had, one row further down (HF-265 D2). */
.version-filter {
  padding: var(--pnl-pad-y) var(--pnl-pad-x);
  border-bottom: 1px solid var(--line);
}

/* The whole row is the hit target, not the 13px box: a checkbox with a separate
   label is the classic control nobody can tap on a phone. */
.version-filter-label {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 32px;
  color: var(--muted);
  font-size: var(--fs-small);
  cursor: pointer;
}

.version-list:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.version-group + .version-group {
  margin-top: 8px;
}

.version-group-day {
  padding: 6px 8px 3px;
  color: var(--muted);
  font-size: var(--fs-small);
  font-weight: 650;
}

/* A grid row: the entry, and the ⋮ that carries the entry's own actions. 44px
   minimum for both, so each is a touch target and not only a mouse target — the
   owner has said mobile is supported, and the ⋮ is painted at every width rather
   than revealed on hover, because a hover affordance is no affordance on a
   phone. */
.version-item {
  display: flex;
  align-items: stretch;
  gap: 2px;
  min-height: 44px;
  border-radius: var(--radius-sm);
  color: var(--ink);
}

.version-item-entry {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  justify-content: center;
  padding: 6px 4px 6px 9px;
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
  cursor: pointer;
}

.version-item-entry:focus-visible,
.version-item-menu:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.version-item-actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
}

/* 44x44, the touch floor, and always visible. `--muted` at rest so a column of
   them does not compete with the version names beside them; full ink once the
   row is where the reader is. */
.version-item-menu {
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.version-item-menu .ms {
  font-size: var(--icon-compact);
}

.version-item:hover .version-item-menu,
.version-item.is-active .version-item-menu {
  color: inherit;
}

.version-item-menu:hover,
.version-item-menu[aria-expanded="true"] {
  background: var(--surface);
  color: var(--ink);
}

/* The row menu. No colour, radius or type of its own: it IS the editor's context
   menu, rendered by the same `menu_render.mjs` the right-click menu uses, so a
   second visual language for the same shape never appears. */
.version-row-menu {
  min-width: 200px;
}

.version-item:hover {
  background: var(--bg-2);
}

.version-item.is-active {
  background: var(--accent-soft);
  color: var(--accent-text);
}

/* The version currently ON SCREEN, which is not always the selected row: a
   reader can arrow past it. A left rule rather than a second fill, so it can
   coexist with the selection colour. */
.version-item.is-previewing {
  box-shadow: inset 2px 0 0 0 var(--accent);
}

.version-item-head {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.version-item-title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-body);
  font-weight: 600;
}

.version-item-pin {
  font-size: var(--icon-compact);
  color: var(--accent-text);
}

.version-item-current,
.version-item-detail {
  color: var(--muted);
  font-size: var(--fs-small);
}

.version-item.is-active .version-item-current,
.version-item.is-active .version-item-detail {
  color: inherit;
}

/* The author's colour (ADR-065): the hue their changes are painted in when
   the version is opened, so the list and the page agree. */
.version-item-author-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-inline-end: 6px;
  border-radius: 50%;
  background: var(--diff-author-color, var(--muted));
  vertical-align: middle;
}

/* The shell's inset, for the reason `.version-filter` takes it: this is the
   "flow bottom" half of the report — the footer's text started 4px inside the
   body's. */
.version-foot {
  padding: var(--pnl-pad-y) var(--pnl-pad-x);
  border-top: 1px solid var(--line);
}

/* ---- The diff canvas (ADR-065) --------------------------------------------
   A comparison is READ on the page — the redline is painted by the engine in
   the author's colour — so the chrome here is only its index: who, "3 of 12"
   with previous/next, a key to what each mark means, and (in Compare) the list.
   Existing tokens only (SKILL §11): the key's samples take the AUTHOR colour
   the page uses, through one custom property the script sets, so the key and
   the marks it explains cannot disagree. */
.diff-nav {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
  min-width: 0;
}
.diff-nav[hidden] { display: none; }
.diff-nav-author {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
}
.diff-nav-author:empty { display: none; }
.diff-author-swatch {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--diff-author-color, var(--muted));
}
.diff-nav-steps {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}
.compare-body .diff-nav {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.diff-nav-step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 28px;
  min-height: 28px;
}
.diff-nav-step:disabled { opacity: 0.45; }
.diff-nav-position {
  font-variant-numeric: tabular-nums;
}
.diff-legend {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  color: var(--muted);
}
.diff-legend-item {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
}
.diff-legend-sample {
  color: var(--diff-author-color, var(--ink));
  text-decoration-color: var(--diff-author-color, var(--ink));
  text-decoration-thickness: 1px;
}
.diff-legend-added .diff-legend-sample { text-decoration-line: underline; }
.diff-legend-removed .diff-legend-sample { text-decoration-line: line-through; }
.diff-legend-moved .diff-legend-sample {
  text-decoration-line: underline;
  text-decoration-style: double;
}
.diff-legend-formatted .diff-legend-sample { color: var(--ink); }
.diff-legend-count { font-variant-numeric: tabular-nums; }
.diff-toggle {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
}
/* The preview bar carries the navigator now, so it wraps rather than clipping
   on a narrow window. */
.version-preview-banner {
  flex-wrap: wrap;
  padding-top: 3px;
  padding-bottom: 3px;
}

/* Compare's panel: the same width as the version panel, because it holds a
   list of sentences a reader reads, not a column of icons. */
.side-panel.compare-panel {
  flex-basis: 320px;
  width: 320px;
  max-width: min(320px, calc(100vw - 52px));
}
.compare-heading { margin: 0 0 4px; font-weight: 600; }
.compare-body .diff-nav { margin: 6px 0; }
.compare-changes {
  list-style: none;
  margin: 6px 0;
  padding: 0;
  display: grid;
  gap: 2px;
}
.compare-change {
  width: 100%;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px 8px;
  align-items: baseline;
  padding: 6px 8px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.compare-change:hover { background: var(--accent-soft); }
.compare-change.is-current {
  border-color: color-mix(in srgb, var(--accent) 42%, var(--line));
  background: var(--accent-soft);
}
.compare-change-kind {
  font-size: var(--fs-small);
  font-weight: 600;
  white-space: nowrap;
}
.compare-change[data-diff-kind="added"] .compare-change-kind { color: var(--review-insert); }
.compare-change[data-diff-kind="removed"] .compare-change-kind { color: var(--review-delete); }
.compare-change[data-diff-kind="moved"] .compare-change-kind { color: var(--review-move); }
.compare-change[data-diff-kind="replaced"] .compare-change-kind { color: var(--accent-text); }
.compare-change[data-diff-kind="formatted"] .compare-change-kind { color: var(--muted); }
.compare-change-text {
  min-width: 0;
  overflow-wrap: anywhere;
}
.compare-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 10px 0 6px;
}
.compare-details {
  margin-top: 8px;
  font-size: var(--fs-small);
}
.compare-details summary { cursor: pointer; color: var(--muted); }

.version-foot-line {
  margin: 0 0 2px;
  font-size: var(--fs-small);
}

.version-foot-muted,
.version-foot-note {
  color: var(--muted);
}

.version-foot-note {
  margin: 0 0 8px;
  font-size: var(--fs-small);
  line-height: 1.45;
}

.version-foot .dialog-button {
  width: 100%;
  justify-content: center;
}

/* A panel that is showing a WINDOW of a long document says so, rather than
   letting a slice read as the whole (the Pages navigator, docs/113 §8.6 — the
   same rule the accessibility mirror follows). */
.panel-window-note {
  margin: 0 0 8px;
  padding: 8px 10px;
  color: var(--faint);
  font-size: var(--fs-small);
  line-height: 1.45;
  border-bottom: 1px solid var(--line);
}

/* One column of page thumbnails; reserve the scrollbar gutter so it never
   overlaps a thumbnail's corner. */
.pages-grid { display: grid; grid-template-columns: 1fr; gap: 12px; align-content: start; scrollbar-gutter: stable; justify-items: center; }
/* Compact thumbnails: a single centered column, each card capped below the
   panel width so it reads as a small page preview with breathing room. */
.page-thumb { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 100%; max-width: 176px; padding: 0; border: none; background: none; cursor: pointer; }
.page-thumb-box { position: relative; width: 100%; display: block; overflow: hidden; background: var(--paper); border: 1px solid var(--line-strong); border-radius: 3px; box-shadow: var(--shadow-sm); }
.page-thumb-canvas { display: block; width: 100%; height: auto; }
.page-thumb-box.is-empty { aspect-ratio: 51 / 66; }
.page-thumb:hover .page-thumb-box { border-color: var(--muted); }
.page-thumb.is-active .page-thumb-box { border-color: var(--accent-text); box-shadow: 0 0 0 2px var(--accent-soft); }
.page-thumb-num { font: 500 10px/1 "JetBrains Mono", ui-monospace, monospace; color: var(--muted); }
.page-thumb.is-active .page-thumb-num { color: var(--accent-strong); }

.review-list { display: grid; gap: 10px; align-content: start; }
.review-composer { display: grid; gap: 6px; padding: 10px; border-bottom: 1px solid var(--line); }
.review-composer label { font-size: var(--fs-small); font-weight: 600; }
.review-composer textarea { width: 100%; box-sizing: border-box; resize: vertical; border: 1px solid var(--line); border-radius: 6px; padding: 7px; font: inherit; font-size: var(--fs-small); background: var(--surface); color: var(--ink); }
.review-composer-actions { display: flex; justify-content: flex-end; gap: 6px; }
.review-mode { display: flex; align-items: center; gap: 4px; padding: 8px 10px 0; font-size: 11px; color: var(--muted); }
.review-mode-button { border: 1px solid var(--line); border-radius: 999px; padding: 3px 8px; background: transparent; color: var(--muted); font: inherit; cursor: pointer; }
.review-mode-button[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent-text); color: var(--accent-strong); }
.review-filters { display: flex; gap: 4px; padding: 8px 10px 0; }
.review-filter { border: 1px solid var(--line); border-radius: 999px; padding: 3px 8px; background: transparent; color: var(--muted); font: inherit; font-size: 11px; cursor: pointer; }
.review-filter[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent-text); color: var(--accent-strong); }
.review-empty, .review-readonly-note { color: var(--muted); font-size: var(--fs-small); line-height: 1.5; }
.review-card { border: 1px solid var(--line); border-radius: 8px; padding: 9px 10px; background: var(--surface); }
.review-card strong, .review-card small { display: block; }
.review-card small { color: var(--muted); font-size: 11px; margin-top: 2px; }
.review-card p { margin: 7px 0 0; color: var(--ink); font-size: var(--fs-small); line-height: 1.45; white-space: pre-wrap; }
.review-card-action { margin-top: 8px; border: 1px solid var(--line); border-radius: 5px; padding: 3px 7px; background: transparent; color: var(--ink); font: inherit; font-size: 11px; cursor: pointer; }
.review-reply { margin-left: 14px; border-left: 3px solid var(--accent-text); }
.review-bulk-actions { display: flex; gap: 6px; padding: 8px 10px 0; }
.review-revision-actions { display: flex; gap: 6px; }

.viewport {
  flex: 1;
  min-width: 0;
  overflow: auto;
  position: relative;
  padding: 12px 0 48px;
  background: var(--bg);
}

.viewport.dragging::after {
  content: "Drop to open";
  position: fixed;
  inset: 116px 16px 16px;
  border: 2px dashed var(--accent-text);
  border-radius: 14px;
  display: grid;
  place-items: center;
  font-weight: 700;
  color: var(--accent-text);
  background: color-mix(in srgb, var(--accent) 9%, transparent);
  pointer-events: none;
  z-index: 5;
}

/* `width: max-content` is what makes the horizontal scroll REACHABLE. A
   centered flex item wider than its container overflows equally on both sides,
   and the left overflow sits before the scroll origin, so it can never be
   scrolled to — the classic centering trap. Sizing the stack to its widest
   child (the sheet, plus the comment column's reserved gutter when that is on)
   grows the scrollable area instead, and `align-items: center` then has room to
   do its job; `min-width: 100%` keeps the stack filling the viewport when the
   sheet is narrower than the window, which is the usual case. */
.pages {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  width: max-content;
  min-width: 100%;
  /* The desk, not the paper: a press out here resolves to no page and does
     nothing, so the arrow is the honest shape (docs/109 HF-179). It rendered
     this way already as `auto`; declaring it makes the intent assertable. */
  cursor: default;
}

/* When the comment column is shown, reserve its width in the page stack's right
   margin so pages stay centered-ish in the remaining space and the comments sit
   in the reserved margin — the same spatial layout as before, now in ONE scroll
   context. (Toggled by JS: `#viewport.has-review-sidebar`.) */
.viewport.has-review-sidebar .pages {
  /* Reserve the comment column's width — but only out of space that exists.
     `.pages` is `width: max-content`, so a fixed reservation is ADDED to the
     sheet's own width rather than taken out of the window: 364px of gutter plus
     an 816px Letter sheet plus the 55px rail needs a 1235px window, and the
     first step-down below was at 860px. Every window between those two numbers
     rendered the page hanging off the right edge, with the comment column —
     absolutely positioned inside this same box — carried off-screen with it.
     The whole band is common laptop territory, which is why this showed up as
     "the layout does not fit in suggesting mode".

     `--page-width` is published by the renderer because the sheet's width is a
     function of paper size and zoom, neither of which a media query can see.
     The fallback is a 100% Letter sheet, so the reservation degrades to the old
     behaviour rather than to zero if it is ever missing. */
  padding-right: clamp(
    0px,
    calc(100% - var(--page-width, 816px) - 12px),
    364px
  );
}

/* The column narrows with the gutter it lives in, down to a floor below which it
   stops being readable; past that it simply overlaps the sheet, which is what
   Docs does at narrow widths and is strictly better than being off-screen. */
.viewport.has-review-sidebar .review-sidebar {
  width: clamp(240px, calc(100% - var(--page-width, 816px) - 24px), 340px);
  min-width: 0;
}

/* The gutter follows the column's own rungs (see the two media queries beside
   `.review-sidebar`). They have to be written HERE, after the rule above:
   media queries contribute no specificity, so the earlier block would lose the
   tie on source order — which is exactly what happened to the 860px rung, whose
   `padding-right: 316px` sat in an unreachable position and never applied.
   At 700px and below there is nothing left to reserve from, so the gutter goes
   entirely and the sheet gets its full measure back. */
@media (max-width: 860px) {
  .viewport.has-review-sidebar .pages {
    padding-right: 316px; /* 300 column + 8 gutter + 8 breathing room */
  }
}

@media (max-width: 700px) {
  .viewport.has-review-sidebar .pages {
    padding-right: 0;
  }
}

/* The bottom sheet (HF-088). `.review-sheet` is set by main.js at
   `REVIEW_SHEET_MAX_WIDTH`, the same width as the rung above. The extra class
   here is deliberate: `.viewport.has-review-sidebar .review-sidebar` a few rules
   up is equally specific, and the 860px rung that "never applied" was lost on
   exactly this tie.

   The sheet owns the bottom half of the screen and nothing else. The page keeps
   its full width and the top half, which is the guarantee the row is about: the
   comments must not swallow the document. It scrolls itself (one card list, not
   a canvas-anchored layer), contains its overscroll so a flick inside it does
   not scroll the document behind it, and is dismissed with the Close button the
   header already carries.

   IT STANDS OFF THE PERMANENT BOTTOM CHROME, rather than taking the bottom edge.
   `inset: auto 0 0 0` is right on a shell whose commands are all at the top; it
   is wrong on this one. Measured at 390x844 with one comment open: the sheet
   occupied 645-844 while `#compactToolbar` is fixed at 773-814 and `.footer` at
   814-844, so opening a comment hid the phone's ENTIRE command surface and its
   status bar — every sampled point across the bar came back as review chrome.
   The bar is how you act on what the sheet is talking about, so a sheet that
   swallows it costs more than it gives.

   `docs/148` §5's region table records the oversight in two adjacent rows: the
   compact toolbar is "docked to the bottom, above the status bar, above the
   keyboard", and four rows later the comment column is "bottom sheet (already,
   at 700px) | HF-088, UNCHANGED". The rung that put a command surface at the
   bottom edge never re-asked what was already parked there. The same table's
   next row states the rule this fix follows — menus hung off the bottom bar open
   as upward sheets, because "there is nothing below a bottom bar to open into".

   Word for mobile stacks its bottom ribbon sheet above the command row and
   Google Docs' sheets sit above the keyboard-attached row. ONLYOFFICE is NOT
   the authority for this one and it would be misleading to cite them: their
   phone chrome is a top Navbar plus toolbars INSIDE the sheet
   (`apps/documenteditor/mobile/src/view/edit/Edit.jsx` opens a Framework7
   `Sheet` at the bottom edge; `apps/common/mobile/resources/less/comments.less`
   puts the comment surface's own toolbar inside it), so they have no global
   bottom row to swallow. Our shell does, deliberately (`docs/148` §5.2), and
   `narrow-review-column.spec.mjs` already declares it PERMANENT chrome that was
   "never the sheet's to give back" — a claim that only holds if this rule holds.

   The offset is the same expression the rest of the phone rung reserves —
   `--phone-bottom-chrome`, the footer plus the command bar plus the keyboard and
   safe-area insets — with `--h-footer` as the fallback for the 621-700px band,
   where the sheet applies (`REVIEW_SHEET_MAX_WIDTH` 700) but `phone-mode` does
   not (`PHONE_MAX_WIDTH` 620) and the footer is the only bottom chrome.

   `max-height` loses the same amount, because "at most half the screen" is a
   claim about the half, not about the sheet: without it a long comment list
   would grow upward past 50vh by exactly the standoff. */
.viewport.review-sheet.has-review-sidebar .review-sidebar {
  position: fixed;
  inset: auto 0 var(--phone-bottom-chrome, var(--h-footer)) 0;
  width: auto;
  min-width: 0;
  max-height: calc(50vh - var(--phone-bottom-chrome, var(--h-footer)));
  padding: 0 10px 10px;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface);
  border-top: 1px solid var(--line);
  box-shadow: var(--shadow-lg);
  z-index: var(--z-drawer);
}

/* The header is the sheet's title bar, so it sticks to the sheet's own top edge
   rather than hanging 8px below the viewport's. */
.viewport.review-sheet .review-sidebar-header {
  top: 0;
  margin-top: 0;
  box-shadow: none;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
}

/* Let the last page scroll clear of the sheet, so a comment anchored to the end
   of the document can still be read with its text on screen.

   `50vh + 12px` is the WORST CASE and it is now exactly that, rather than an
   over-estimate: the sheet's own extent is at most `50vh - standoff` and it sits
   `standoff` off the bottom, so the two sum back to 50vh whatever the rung.
   A SHORT sheet is still over-reserved — one comment is a 200px sheet against a
   434px reservation at 390x844, which is 234px of dead scroll below a short
   document. Tightening that needs the sheet's MEASURED height, which CSS cannot
   ask for; `phone_chrome.mjs` already publishes measured numbers as custom
   properties (`--phone-keyboard-inset`) and is where a `--review-sheet-height`
   would belong. Recorded rather than guessed at with a smaller constant, which
   would trade dead scroll for an unreadable last comment. */
.viewport.review-sheet.has-review-sidebar .pages {
  padding-bottom: calc(50vh + 12px);
}

/* The live W x H bubble shown while an object is being resized. Anchored to the
   preview's bottom-right, which is where the pointer already is on the most
   common (SE) handle, and nudged inside the box so it never leaves the page. */
.object-resize-readout {
  position: absolute;
  right: 0;
  bottom: 0;
  transform: translate(-4px, -4px);
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  background: var(--accent);
  /* --accent-ink, not #ffffff: accent fills are read against the ink token, and
     a literal here has no dark value by construction (tests/style_tokens). */
  color: var(--accent-ink);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  line-height: 1.4;
  white-space: nowrap;
  pointer-events: none;
}

.pages:focus {
  outline: none;
}

/* ---- Horizontal ruler (Word/Docs style: a subtle strip with a lighter content
   span between the page margins, hairline ticks, and pentagon indent markers) - */
.ruler {
  position: sticky;
  top: 0;
  z-index: 4;
  height: 24px;
  /* Cancel most of the .pages flex gap (22px) so the ruler hugs the page top. */
  margin-bottom: -16px;
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: 5px;
  user-select: none;
  /* The strip itself never intercepts a page click; only the small indent
     markers below opt back in (pointer-events: auto) so they can be dragged. */
  pointer-events: none;
}

.ruler[hidden] {
  display: none;
}

.ruler-track {
  position: relative;
  height: 100%;
}

/* The content span between the page margins reads a shade lighter than the
   margin zones — the same relationship in light and dark. */
.ruler-content {
  position: absolute;
  top: 3px;
  bottom: 3px;
  background: var(--surface);
  border-radius: 2px;
  box-shadow: inset 0 0 0 1px var(--line);
  /* Click the content strip to drop a tab stop at that position. */
  pointer-events: auto;
  cursor: copy;
}

/* Word-style tab-type box at the ruler's left; shows/cycles the next tab type. */
.tab-corner {
  position: absolute;
  left: 0;
  top: 3px;
  width: 17px;
  height: 17px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  background: var(--surface);
  color: var(--ink);
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  pointer-events: auto;
}

.tab-corner:hover {
  border-color: var(--accent-text);
  color: var(--accent-text);
}

/* A tab stop glyph sitting on the lower edge of the ruler. */
.tab-glyph {
  position: absolute;
  bottom: 1px;
  transform: translateX(-50%);
  padding: 0 1px;
  font-size: 9px;
  font-weight: 800;
  line-height: 1;
  color: var(--accent-text);
  cursor: grab;
  pointer-events: auto;
}

.tab-glyph:hover {
  filter: brightness(1.15);
}

.ruler-tick {
  position: absolute;
  top: 4px;
  width: 1px;
  background: var(--muted);
}

.ruler-tick.major {
  height: 6px;
  opacity: 0.7;
}

.ruler-tick.minor {
  height: 3px;
  opacity: 0.4;
}

.ruler-num {
  position: absolute;
  top: 12px;
  font-size: var(--fs-micro);
  line-height: 1;
  color: var(--muted);
  transform: translateX(-50%);
  font-variant-numeric: tabular-nums;
}

/* Pentagon indent markers (a rectangle body + a pointed tip, like Word/Docs).
   First-line points down from the top; left/right point up from the bottom. */
.ruler-marker {
  position: absolute;
  width: 9px;
  height: 11px;
  transform: translateX(-50%);
  background: var(--accent);
  cursor: ew-resize;
  /* The only interactive part of the ruler — grab to drag the paragraph indent. */
  pointer-events: auto;
  touch-action: none;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.28));
}

/* Widen the grab target horizontally only, so it never reaches down into the
   page area below the ruler. */
.ruler-marker::after {
  content: "";
  position: absolute;
  inset: -2px -6px;
}

.ruler-marker.down {
  top: 2px;
  clip-path: polygon(0 0, 100% 0, 100% 52%, 50% 100%, 0 52%);
}

.ruler-marker.up {
  bottom: 2px;
  clip-path: polygon(50% 0, 100% 48%, 100% 100%, 0 100%, 0 48%);
}

.ruler-marker:hover,
.ruler-marker.dragging {
  filter: brightness(1.12) drop-shadow(0 1px 1px rgba(0, 0, 0, 0.28));
}

/* The wrap carries the page-box geometry (its width/height are set in JS from
   the page size in twips) and the sheet chrome, so it holds its space and looks
   like a blank page even when its canvas is virtualized away off-screen. The
   live raster canvas, when mounted, fills it exactly. */
/* No `max-width` here, deliberately. The wrap's width AND height are both set
   inline in CSS px from the section's page size in twips, so any width clamp
   that the height does not follow rescales one axis only — the sheet then
   renders at the wrong aspect ratio and every glyph is horizontally compressed,
   silently, with no error (HF-083). The previous `max-width: 96vw` did exactly
   that in any window narrower than the sheet: split screen, tablet, phone.
   A page has a real width, so when it does not fit we do what Word does and let
   `.viewport` scroll horizontally (`.pages` below reserves the room for it);
   a reader who would rather scale than scroll has Fit width in the zoom menu,
   which recomputes the factor on every resize and keeps both axes together. */
/* The one element the page sheets live in, and the element whose height IS the
   scroll range (docs/113 §8.6). Its height is capped at MAX_SCROLL_PX because a
   browser stops scrolling at 2^24 CSS px, and the sheets are positioned inside
   it by `page_scroll.mjs` rather than stacked in flow — that is what makes the
   scroll height independent of the page count. It is a flex item of `.pages`,
   so the existing centering, `width: max-content` sizing and comment-gutter
   reservation all apply to it exactly as they applied to a sheet. */
.page-band {
  position: relative;
  flex: none;
}

.page-wrap {
  position: relative;
  background: var(--paper);
  box-shadow: var(--shadow-page);
  border-radius: 3px;
  line-height: 0;
}

/* REFLOW (docs/151 §6.2, corrected here; ADR-046): there is no
   page, so there is no desk either.
   -------------------------------------------------------------------------
   In reflow the engine cuts the galley into fixed-height tiles at LINE
   boundaries, so a tile edge falls wherever 11in of column happens to end —
   usually in the middle of a paragraph. The paper affordances therefore have to
   go, and each one is a separate lie if it stays:

     box-shadow    draws a sheet edge across a sentence;
     border-radius rounds a corner that is not a corner of anything;
     gap           is the desk between two sheets, and there is no desk. It is
                   `buildPageBand`'s `gap: 0` rather than CSS, because the tile
                   positions have to be computable for tiles that are not in the
                   DOM (see the note below).

   THAT WAS NOT ENOUGH, AND THE OLD COMMENT HERE SAID SO WITHOUT NOTICING. It
   claimed that what is left is "one continuous column of paper-coloured raster,
   which is what the reader should see". It is an accurate description of what
   those three lines produce and an inaccurate description of a pageless view:
   a white rectangle inside a differently-coloured field is a page whichever
   two properties you file off it. Measured before this rule existed, at a
   1440px window in Reading: a 500px `--paper` column centred in 940px of
   `--bg`, which our own `reflow.spec.mjs` asserted as "real desk on each side".
   ONLYOFFICE's reader keeps that same page-on-desk structure (#fff page,
   #EEEEEE/#666666 canvas, 20mm gaps, borders) — so the view was built against
   the wrong reference on its most visible axis.

   So the desk goes too. `--paper-surface` fills the scroller edge to edge and
   the tiles sit on it in their own colour, which is the same colour; the
   measure is then where the TEXT is rather than where the paper is, and a tile
   boundary has nothing to draw itself against. Two consequences worth stating
   because they are the point rather than side effects:

     * a seam between two tiles becomes unpaintable, not merely unpainted —
       surface and raster are one value by construction (see `--paper-surface`);
     * the space beside the measure is surface, not desk, so it is somewhere an
       affordance may legitimately live. `fold_chrome.mjs`'s disclosure chevron
       is the first thing to use that, and it is why the chevron is reachable in
       reflow at all.

   The class is written by `reflow_chrome.mjs`. */
#viewport.is-reflow {
  background: var(--paper-surface);
  /* The thumb follows the track it runs on, not the theme — see
     `--paper-scrollbar-thumb`. Firefox's property here, WebKit's below. */
  scrollbar-color: var(--paper-scrollbar-thumb) transparent;
}

#viewport.is-reflow::-webkit-scrollbar-thumb {
  background: var(--paper-scrollbar-thumb);
  background-clip: padding-box;
}

#viewport.is-reflow::-webkit-scrollbar-thumb:hover {
  background: var(--paper-scrollbar-thumb-hover);
  background-clip: padding-box;
}

#viewport.is-reflow .page-wrap {
  background: var(--paper-surface);
  box-shadow: none;
  border-radius: 0;
}

/* The raster canvas itself. A 3px radius clipped the corner pixels of a tile
   that has no corner — invisible against a desk of the same colour, and still a
   page affordance asserted in the one place a pixel test would look. */
#viewport.is-reflow .page {
  border-radius: 0;
  /* SEAMLESS TILES (docs/151 §4.4b): the band puts every tile on the raster's
     own pixel grid at exactly its raster's size (`page_scroll.mjs`), so the
     canvas is drawn 1:1. `auto` keeps it at its own aspect ratio rather than
     stretching it to the box — if the box were ever a row off the raster, the
     raster would still not be resampled, and the row would fall under the
     next tile, which is laid over it by construction. */
  height: auto;
}

/* The overlay may paint past its tile only as far as the fold chevron needs
   (`--reflow-overlay-spill`, written by `reflow_chrome.mjs`: up to 24px, never
   more than the surface beside the measure, 0 at the phone rung). A table
   scrolled sideways (docs/151 §6.3d) can put its caret, selection or cell
   outline wholly outside the tile; clipped here, they neither float in the
   margin nor widen `#viewport` into a horizontal scroll. `clip` and not
   `hidden`, because a clip does not count toward the scroller's overflow — and
   HORIZONTAL only: vertically a tile's overlay must still reach over its
   neighbour (a touch selection handle below the last line of a tile, an
   object's rotate handle above the first), which `overflow-x: clip` leaves
   `visible` where `hidden` would have forced it to `auto`. */
#viewport.is-reflow .page-wrap > .overlay {
  overflow-x: clip;
  overflow-clip-margin: var(--reflow-overlay-spill, 0px);
}

/* WIDE TABLES SCROLL SIDEWAYS (docs/151 §6.3d) — Google Docs' pageless answer,
   "you can create wide tables and view them by scrolling left and right".
   `reflow_table_scroll.mjs` lays one of these over each over-wide table's band
   on a tile: a native horizontal scroller holding the table rasterised once at
   its full width, so the compositor does the scrolling. It spans the whole
   tile, gutters included, exactly like a CSS scroll container whose padding is
   inside its scrollport; it sits BEFORE the overlay in the wrap so the caret
   and the selection stay on top of it. Vertical gestures chain to the page,
   because it cannot scroll vertically. */
.reflow-table-scroll {
  position: absolute;
  left: 0;
  right: 0;
  /* The tile's own cursor, which `reflow_table_scroll.mjs` copies here from the
     hover router's answer on the canvas it covers; text until there is one. */
  cursor: text;
}

.reflow-table-scroll__port {
  width: 100%;
  height: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  /* The native bar would take its height out of the table's last row; the
     overlay bar below replaces it and is always visible (docs/151 A-3). */
  scrollbar-width: none;
  background: var(--paper-surface);
}

.reflow-table-scroll__port::-webkit-scrollbar {
  display: none;
}

.reflow-table-scroll__port,
.reflow-table-strip {
  cursor: inherit;
}

.reflow-table-strip {
  display: block;
}

/* Always visible whenever the table overflows — a hover-only bar is an
   invisible affordance for the one thing a reader must discover (A-3). The
   visible bar is 6px; the press target is the 14px box it sits in. */
.reflow-table-scroll__bar {
  position: absolute;
  left: 6px;
  right: 6px;
  bottom: 0;
  height: 14px;
  cursor: default;
  touch-action: none;
}

.reflow-table-scroll__bar::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 2px;
  height: 6px;
  border-radius: 3px;
  background: color-mix(in srgb, var(--paper-ink) 7%, transparent);
}

.reflow-table-scroll__thumb {
  position: absolute;
  left: 0;
  bottom: 2px;
  height: 6px;
  border-radius: 3px;
  background: var(--paper-scrollbar-thumb);
}

.reflow-table-scroll__bar:hover .reflow-table-scroll__thumb {
  background: var(--paper-scrollbar-thumb-hover);
}

/* Inside the band a sheet is placed, not flowed: `main.js` writes `top`/`left`
   from the document-space page stack. The 22px pitch between pages lives in
   `PAGE_GAP_PX` (page_scroll.mjs) instead of in a flex `gap`, because the
   positions have to be computable for pages that are not in the DOM. */
.page-band > .page-wrap {
  position: absolute;
}

/* `cursor: text` is only the STARTING point. A sheet is one canvas, so CSS
   cannot tell a picture from a paragraph from a checkbox inside it; the hover
   router (`pointer_cursor.mjs`, docs/109 HF-179) writes an inline cursor here
   from what the engine says is under the pointer, and an inline style beats
   this declaration. This stays so that a sheet with no router answer yet — the
   frame before the first probe resolves — still reads as text rather than as
   an arrow. The old `.page.link-hover` class is gone with the router: it was
   the one thing on the whole surface that ever changed. */
.page {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 3px;
  cursor: text;
}

/* Selection / caret layer over the (white) canvas — fixed light-surface colors. */
.overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.overlay .highlight {
  position: absolute;
  background: color-mix(in srgb, var(--accent) 24%, transparent);
  border-radius: 1px;
}

/* The spelling squiggle (docs/114 §5.7). Drawn in the overlay, over the raster,
   so it costs the layout engine nothing and moves with the text for free.
   `pointer-events: none` for the same reason `.review-comment-marker` used to
   need fixing (REVIEW-GAP-005): a marker that swallowed the right-click would
   break caret placement, and the flagged word is resolved from the caret anchor
   rather than from this element.
   Two diagonal gradients rather than a data-URI SVG, so there is no colour
   literal hidden in an encoded string and no extra request. */
.overlay .spell-error,
.overlay .grammar-error {
  --proofing-mark: var(--paper-spelling);
  position: absolute;
  pointer-events: none;
}
.overlay .grammar-error {
  --proofing-mark: var(--paper-grammar);
}
.overlay .spell-error::after,
.overlay .grammar-error::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  bottom: -1px;
  height: 3px;
  background-image:
    linear-gradient(45deg, transparent 40%, var(--proofing-mark) 40% 60%, transparent 60%),
    linear-gradient(-45deg, transparent 40%, var(--proofing-mark) 40% 60%, transparent 60%);
  background-size: 4px 3px;
  background-repeat: repeat-x;
  background-position: 0 100%, 2px 100%;
}

/* Comment range marker: interaction-only highlight over the fixed canvas.
 * `--review-author-color` (set per marker, docs/81 REVIEW-GAP-015) tints it to
 * the comment author's stable palette color; the amber default keeps a comment
 * recognizable when no author is attributed. */
.overlay .review-comment-marker {
  --review-author-color: var(--paper-comment);
  position: absolute;
  background: color-mix(in srgb, var(--review-author-color) 22%, transparent);
  border-bottom: 2px solid color-mix(in srgb, var(--review-author-color) 75%, transparent);
  pointer-events: auto;
  cursor: pointer;
}
.overlay .review-comment-marker-active {
  background: color-mix(in srgb, var(--review-author-color) 40%, transparent);
  border-bottom-color: var(--review-author-color);
}
/* Revision markers carry `--review-author-color` per author. `pointer-events:
 * auto` lets the native attribution `title` tooltip appear on hover; the
 * pointerdown still bubbles to the page's own hit-testing (the same pattern the
 * comment marker uses), so caret placement inside a tracked change is unaffected
 * (REVIEW-GAP-005). Insertion = author-colored underline; deletion = author-
 * colored tint + underline, preserving the fill-vs-underline distinction. */
/* `cursor: text` is load-bearing, not decoration. The marker takes pointer
 * events for its tooltip but is NOT a control: the pointerdown falls through to
 * caret placement underneath. Without this it inherited `auto` from the overlay
 * layer — which is an ARROW over a non-text element — so hovering any tracked
 * change promised the one thing it does not do (docs/109 HF-179). */
.overlay .review-revision-marker { --review-author-color: var(--paper-insert); position: absolute; pointer-events: auto; cursor: text; }
.overlay .review-insertion-marker { border-bottom: 2px solid color-mix(in srgb, var(--review-author-color) 85%, transparent); }
.overlay .review-deletion-marker { background: color-mix(in srgb, var(--review-author-color) 18%, transparent); border-bottom: 2px solid color-mix(in srgb, var(--review-author-color) 85%, transparent); }
/* Paragraph-level revisions (docs/108). A mark revision is the pilcrow that ends
   the paragraph, drawn in the author's colour — underlined when a break was
   added, struck through when one was deleted, matching the run markers above.
   A formatting change is a bar in the margin, Word's "changed lines" bar. */
.overlay .review-paragraph-mark-marker { display: flex; align-items: center; color: color-mix(in srgb, var(--review-author-color) 90%, var(--ink)); font-weight: 700; line-height: 1; }
.overlay .review-paragraph-mark-marker::after { content: "¶"; font-size: 0.8em; }
.overlay .review-paragraph-mark-inserted { border-bottom: 2px solid color-mix(in srgb, var(--review-author-color) 85%, transparent); }
.overlay .review-paragraph-mark-deleted { text-decoration: line-through; background: color-mix(in srgb, var(--review-author-color) 18%, transparent); }
.overlay .review-paragraph-format-bar { background: color-mix(in srgb, var(--review-author-color) 85%, transparent); border-radius: 1px; }
.overlay .review-revision-marker-active { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 70%, transparent); }

.overlay .table-cell-selection {
  position: absolute;
  border: 1px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent);
}

/* Object selection chrome (docs/85 §3.3): an outline + eight resize/move grips,
 * drawn by us from the object's placed rect so they match the raster with zero
 * drift. The grips are display-only this slice; drag-resize is the next slice. */
.overlay .object-outline {
  position: absolute;
  border: 1.5px solid var(--accent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 30%, transparent);
  pointer-events: none;
}

/* An object held ALONGSIDE the primary selection (Ctrl/⌘+click), so a
   multi-object selection is visible before Group is pressed. Dashed rather than
   solid: one object is the subject of the kind-specific commands, and the chip
   is anchored to it. */
.overlay .object-outline.is-co-selected {
  border-style: dashed;
  box-shadow: none;
}

.overlay .object-handle {
  position: absolute;
  width: 9px;
  height: 9px;
  margin: -5px 0 0 -5px; /* center on the handle point */
  background: var(--surface);
  border: 1.5px solid var(--accent);
  border-radius: 2px;
  box-shadow: 0 1px 2px rgba(20, 22, 28, 0.3);
  pointer-events: auto; /* drag to resize (P1G-OBJ-GEOMETRY) */
  touch-action: none;
}

/* The grip stays 9px (12px for crop) because that is the drawn chrome Word and
   Docs both use — but the TARGET must not be 9px. WCAG 2.5.8 asks for 24x24,
   and a finger or pen never reliably lands inside a 9px square, so resizing an
   image was effectively pointer-only (HF-098). A pseudo-element grows the hit
   area without moving a pixel of the visual, the same trick `.ruler-marker
   ::after` already uses for the indent markers.
   It grows OUTWARD ONLY, and that direction is the whole design. The eight
   grips sit on the object's perimeter, so on a small object they are only a few
   pixels apart — the demo document has a 16px-wide image whose grips are 8px
   apart, closer together than a 9px grip is wide. A symmetric `inset: -8px`
   therefore put the South grip's target over the South-East grip's centre, and
   because South paints later it won: dragging the corner resized the wrong
   axis. Growing away from the object instead keeps every target clear of every
   other grip's centre for any object wider than the grips themselves, while
   still reaching 24px on the axis the grip is dragged along — which is the axis
   that matters, since a North grip is never dragged sideways.
   Finger-sized (44px) grips need more than this: they would have to overlap and
   be arbitrated by distance in the pointerdown handler, which is main.js. */
.overlay .object-handle {
  --grip-grow: 15px; /* 9px grip + 15px = 24px, the WCAG 2.5.8 target */
}

.overlay .object-crop-handle {
  --grip-grow: 12px; /* 12px grip + 12px = 24px */
}

.overlay .object-handle::after,
.overlay .object-crop-handle::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* North-facing grips (NW, N, NE) grow up; and so on clockwise. Corner grips
   appear in two lists and so grow on both of their outward axes. */
.overlay :is(.object-handle, .object-crop-handle)[data-handle="0"]::after,
.overlay :is(.object-handle, .object-crop-handle)[data-handle="1"]::after,
.overlay :is(.object-handle, .object-crop-handle)[data-handle="2"]::after {
  top: calc(-1 * var(--grip-grow));
}

.overlay :is(.object-handle, .object-crop-handle)[data-handle="2"]::after,
.overlay :is(.object-handle, .object-crop-handle)[data-handle="3"]::after,
.overlay :is(.object-handle, .object-crop-handle)[data-handle="4"]::after {
  right: calc(-1 * var(--grip-grow));
}

.overlay :is(.object-handle, .object-crop-handle)[data-handle="4"]::after,
.overlay :is(.object-handle, .object-crop-handle)[data-handle="5"]::after,
.overlay :is(.object-handle, .object-crop-handle)[data-handle="6"]::after {
  bottom: calc(-1 * var(--grip-grow));
}

.overlay :is(.object-handle, .object-crop-handle)[data-handle="6"]::after,
.overlay :is(.object-handle, .object-crop-handle)[data-handle="7"]::after,
.overlay :is(.object-handle, .object-crop-handle)[data-handle="0"]::after {
  left: calc(-1 * var(--grip-grow));
}

/* Directional resize cursors, keyed by handle index (NW,N,NE,E,SE,S,SW,W). */
.overlay .object-handle[data-handle="0"],
.overlay .object-handle[data-handle="4"] { cursor: nwse-resize; }
.overlay .object-handle[data-handle="2"],
.overlay .object-handle[data-handle="6"] { cursor: nesw-resize; }
.overlay .object-handle[data-handle="1"],
.overlay .object-handle[data-handle="5"] { cursor: ns-resize; }
.overlay .object-handle[data-handle="3"],
.overlay .object-handle[data-handle="7"] { cursor: ew-resize; }

/* The move pad over a selected, movable object: invisible, and there purely so
 * the object's body carries `touch-action: none` the way its grips already do.
 * Without it the browser claims a finger drag for scrolling at touch-start and
 * the move never begins.
 *
 * It carries NO `z-index`, deliberately, and is painted before the grips: a
 * positioned element at `z-index: auto` is hit-tested in DOM order, so the
 * grips stay on top. Lifting the pad to `z-index: 1` put it over the half of
 * each grip that overlaps the object, and an edge drag resized nothing because
 * the pad answered the press — caught by the margin-snap guard. */
.overlay .object-move-pad {
  position: absolute;
  background: transparent;
  pointer-events: none;
  touch-action: none;
}

/* The pad is INERT unless the primary pointer is coarse, and that is not a
 * nicety — a pad that takes mouse presses is a sheet of glass over the object.
 * The page's own hit-test is what resolves a double-click into a group, a
 * right-click into the context menu and a click into the right nested object,
 * and an element over the canvas intercepts all of it: with `pointer-events:
 * auto` everywhere, four nested-object specs and three object-command ones
 * failed, most of them by timing out on a click that could not reach the page.
 * A mouse has that hit-test to fall through to. A finger does not — its gesture
 * is gone at touch-start — so the pad is live exactly where the fallback is not.
 * Same query the touch-target rules already key on. */
@media (pointer: coarse) {
  .overlay .object-move-pad {
    pointer-events: auto;
    cursor: move;
  }
}

/* Alignment guides (Word's green / Docs' blue): the line the dragged object has
 * been pulled onto, drawn across the whole page so the relationship is visible
 * rather than inferred. Host chrome only — it never commits anything, and it
 * exists only while a pointer is down. */
.overlay .object-align-guide {
  position: absolute;
  background: var(--accent);
  pointer-events: none;
  z-index: 2;
}
.overlay .object-align-guide[hidden] { display: none; }
.overlay .object-align-guide.is-vertical {
  top: 0;
  bottom: 0;
  width: 1px;
  margin-left: -0.5px;
}
.overlay .object-align-guide.is-horizontal {
  left: 0;
  right: 0;
  height: 1px;
  margin-top: -0.5px;
}

/* Live drag-resize preview outline (host chrome; the op commits once on release).
   `transform-origin: center` because a rotated object's preview is turned about
   its own middle, exactly as the engine paints the object. */
.overlay .object-resize-preview {
  position: absolute;
  border: 1.5px dashed var(--accent);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  transform-origin: center;
  pointer-events: none;
}

/* Where an in-line object will land when a drag in the text is released
   (`docs/109` UX-OB-02). The accent, not the ink of the text caret, so the two
   are never confused while both are on screen, and it does not blink: it
   follows the pointer. A copy (Ctrl, or Option on a Mac) wears a plus, Word's
   own sign for a copying drag. Positioned by `object_text_move.mjs` from the
   engine's `caretRect`. */
.overlay .object-drop-caret {
  position: absolute;
  width: 2px;
  margin-left: -1px;
  background: var(--accent);
  pointer-events: none;
}

.overlay .object-drop-caret[data-copy="true"]::after {
  content: "+";
  position: absolute;
  left: 4px;
  top: -10px;
  font-weight: 700;
  line-height: 1;
  color: var(--accent);
}

/* The object's outline is TURNED with the object rather than redrawn as its
   bounding box, so `transform-origin` has to be its own centre here too. */
.overlay .object-outline {
  transform-origin: center;
}

/* ---- Free rotation (ONLYOFFICE/Word parity) ------------------------------
 * The rotation grip sits above the object's top edge, on the object's own axis,
 * exactly where ONLYOFFICE puts theirs (`TRACK_DISTANCE_ROTATE`, 25 screen px;
 * 24 here so the tether is one 24px WCAG target tall and leaves no dead band
 * between it and the north grip's zone).
 *
 * It is ROUND where the resize grips are square, which is the only thing
 * distinguishing them at 9px, and it is how all three products draw it. */
.overlay .object-rotate-handle {
  border-radius: 50%;
  /* `crosshair` is ONLYOFFICE's own answer here (`CommonController.js:1531`).
     Word and Docs use a dedicated rotate bitmap; a data-URI cursor would be a
     visual change for the owner to approve (`docs/63`), so the honest keyword
     ships and the row in `pointer_cursor.mjs` says so. */
  cursor: crosshair;
}

.overlay .object-rotate-handle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The line from the object's edge up to the grip. Without it a dot floating
   24px above a corner reads as a stray mark rather than as this object's
   control — all three products draw the tether. `transform-origin: bottom` so
   the line pivots on the point it is anchored to, which is on the object. */
.overlay .object-rotate-tether {
  position: absolute;
  width: 1.5px;
  margin-left: -0.75px;
  background: var(--accent);
  transform-origin: bottom center;
  translate: 0 -100%;
  pointer-events: none;
}

/* The live rotation preview: the object's frame, turned to the angle the
   pointer is asking for. Solid rather than dashed, because during a rotation
   the only thing that is changing is the ANGLE, and a dashed box at 3 degrees
   off true is hard to read against the object beneath it. */
.overlay .object-rotate-preview {
  position: absolute;
  border: 1.5px solid var(--accent);
  transform-origin: center;
  pointer-events: none;
}

/* The live angle, in the same chip the size readout uses so the two gestures
   read as one family. Centred above the box rather than in the corner: a
   rotation has no corner the pointer is reliably near. */
.object-rotate-readout {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translate(-50%, -140%);
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  line-height: 1.4;
  white-space: nowrap;
  pointer-events: none;
}

/* Image crop chrome (Word/Docs-style direct manipulation). The removed margins
 * are dimmed by four strips; the kept rectangle stays clear over the canvas
 * image beneath, bordered, with black crop handles dragged to adjust it. */
.overlay .object-crop-dim {
  position: absolute;
  background: rgba(20, 22, 28, 0.55);
  pointer-events: none;
}
.overlay .object-crop-rect {
  position: absolute;
  border: 1px solid rgba(255, 255, 255, 0.9);
  box-shadow: 0 0 0 1px rgba(20, 22, 28, 0.55);
  pointer-events: none;
}
.overlay .object-crop-handle {
  position: absolute;
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px; /* center on the handle point */
  background: var(--paper-ink);
  border: 2px solid var(--paper);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
  pointer-events: auto;
  touch-action: none;
}
.overlay .object-crop-handle[data-handle="0"],
.overlay .object-crop-handle[data-handle="4"] { cursor: nwse-resize; }
.overlay .object-crop-handle[data-handle="2"],
.overlay .object-crop-handle[data-handle="6"] { cursor: nesw-resize; }
.overlay .object-crop-handle[data-handle="1"],
.overlay .object-crop-handle[data-handle="5"] { cursor: ns-resize; }
.overlay .object-crop-handle[data-handle="3"],
.overlay .object-crop-handle[data-handle="7"] { cursor: ew-resize; }
/* Active state for the Crop → Apply button while a crop session is live. */
.object-bar-btn.is-active {
  background: var(--accent);
  color: var(--accent-ink);
}

/* Placeholder object context bar (docs/85 §4.1): names the selected object and
 * the actions the later slices will make real. */
.object-context-bar {
  position: fixed;
  z-index: var(--z-selection-toolbar);
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 10px;
  border-radius: var(--radius-popover);
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-popover);
  font-size: var(--fs-small);
  pointer-events: auto;
}
.object-context-bar[hidden] { display: none; }
/* Never wider than the window; the bar's own row wraps before anything inside
   it is squeezed. */
.object-context-bar {
  max-width: calc(100vw - 16px);
  flex-wrap: wrap;
}
/* The drag hint is the first thing to go when the window is narrow: it repeats
   an affordance the handles already show, and the controls do not. */
@media (max-width: 720px) {
  .object-context-bar small { display: none; }
  /* On a phone the bar may be three rows: the object is small, the page around
     it is mostly empty, and a control clipped off the right-hand edge is one
     the user cannot reach at all. Height is the cheaper cost. */
  /* Selected through the bar so this outranks the desktop rule below it: a
     media query adds no specificity, and the later rule would otherwise win. */
  .object-context-bar .object-wrap-menu {
    /* Its own row, wrapping inside itself: `flex: 0 0 auto` is what keeps it
       from being squeezed on a desktop, and is exactly what clips it here. */
    flex: 1 1 100%;
    flex-wrap: wrap;
    min-width: 0;
  }
  .object-context-bar .object-bar-actions {
    flex-wrap: wrap;
  }
}
/* Icon-only chip buttons keep a 28px target (WCAG 2.5.8 asks 24). */
.object-bar-btn { white-space: nowrap; }
.object-bar-icon {
  padding: 3px 6px;
  min-width: 28px;
  justify-content: center;
}
.object-context-bar strong { text-transform: capitalize; }
.object-context-bar small { color: var(--muted); }
/* Seven chips now that "In line" is a mode. The ROW must not wrap and must not
   be squeezed: the bar is placed ABOVE the object and falls back to overlapping
   it when there is no room, so every extra line of bar is a line of the object
   the user cannot reach. The BAR wraps instead, which only happens on a phone.
   */
.object-wrap-menu {
  display: flex;
  flex: 0 0 auto;
  flex-wrap: nowrap;
  gap: 3px;
}
.object-wrap-btn { white-space: nowrap; }
.object-wrap-btn:disabled {
  cursor: not-allowed;
  color: var(--faint);
  border-color: var(--line);
}
.object-wrap-btn {
  border: 1px solid var(--line);
  border-radius: 6px;
  /* 24px is the WCAG 2.5.8 target minimum. These chips were 21 tall before "In
     line" joined them as a seventh; a row that is the primary way to change how
     an image sits has to be tappable. */
  min-height: 24px;
  padding: 3px 8px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 11px;
  cursor: pointer;
}
.object-wrap-btn:hover { background: var(--accent-soft); border-color: var(--accent-text); }
.object-wrap-btn[aria-pressed="true"] {
  background: var(--accent-soft);
  border-color: var(--accent-text);
  color: var(--accent-strong);
  font-weight: 650;
}

.object-bar-divider {
  width: 1px;
  align-self: stretch;
  margin: 2px 1px;
  background: var(--line);
}
.object-bar-actions { display: flex; gap: 3px; }
.object-bar-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 3px 8px 3px 6px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 11px;
  cursor: pointer;
}
.object-bar-btn .ms { font-size: 15px; }
.object-bar-btn:hover { background: var(--accent-soft); border-color: var(--accent-text); }
.object-bar-btn.danger { color: var(--danger); }
.object-bar-btn.danger:hover { background: var(--danger-soft); border-color: var(--danger); }

/* ---- Object alt-text / crop dialogs -------------------------------------- */
.object-dialog { width: min(440px, 100%); }
/* The third hand-built stacked form, on `.link-body`'s rhythm for the same
   reason: a flat 12px gap put the alt-text label exactly as far from its own
   textarea as from the note under it. */
.object-dialog-body {
  --dlg-bind: calc(6px - var(--space-4));
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.object-dialog-body > label + *,
.object-dialog-body > .dialog-field-note {
  margin-top: var(--dlg-bind);
}

.object-dialog-body > label {
  color: var(--muted);
  font-size: var(--fs-small);
  font-weight: 600;
}
.object-dialog-body textarea {
  width: 100%;
  min-height: 84px;
  padding: 8px 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-body);
  font-weight: 450;
  resize: vertical;
}
.object-dialog-body textarea:focus {
  outline: none;
  border-color: var(--accent-text);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
#altTextNote.error,
#cropNote.error {
  color: var(--danger);
  font-weight: 550;
}

.overlay .table-col-resize-handle {
  position: absolute;
  width: 10px;
  margin-left: -5px;
  min-height: 18px;
  cursor: col-resize;
  pointer-events: auto;
  touch-action: none;
}

.overlay .table-col-resize-handle::after {
  content: "";
  position: absolute;
  inset: 0 4px;
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  opacity: 0;
}

.overlay .table-col-resize-handle:hover::after {
  opacity: 1;
}

.overlay .table-col-resize-preview {
  position: absolute;
  width: 2px;
  margin-left: -1px;
  background: var(--accent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--surface) 80%, transparent);
  pointer-events: none;
}

/* The ROW guide (docs/141 D-1). The same rule turned through 90 degrees, from the
   same token, because it is the same affordance on the other axis — and it spans
   the TABLE, not the page: the column guide used to take its height from
   `page.overlay.clientHeight` and drew a full-page rule for a three-row table. */
.overlay .table-row-resize-preview {
  position: absolute;
  height: 2px;
  margin-top: -1px;
  background: var(--accent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--surface) 80%, transparent);
  pointer-events: none;
}

/* Touch boundary pills (docs/141 D-1 §4.1.7). Hover does not exist on touch, so
   the affordance has to be a visible object a finger can land on: 24 CSS px
   across the boundary, WCAG 2.5.8 Target Size (Minimum). One pill per boundary
   rather than a continuous strip, so two adjacent boundaries never merge into one
   target. Armed by a tap inside the table and cleared with the caret. */
.overlay .table-row-touch-pill,
.overlay .table-col-touch-pill {
  position: absolute;
  pointer-events: auto;
  touch-action: none;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
}

.overlay .table-row-touch-pill {
  cursor: row-resize;
}

.overlay .table-col-touch-pill {
  cursor: col-resize;
}

/* Touch SELECTION handles and the loupe (`105` UX-018, `148` §9 item 2).

   Draw small, hit big — the fifth surface in this file to make that separation
   and the same one ONLYOFFICE make (`MOBILE_SELECT_TRACK_ROUND = 14` drawn
   inside a `TrackTargetEps = 20` reach). The BOX is the target and carries
   `touch-action: none`, because the browser latches its pan decision from the
   hit element at touch-start and nothing a handler does afterwards can take it
   back — the same trap the object move pad records. The DOT is a 14px circle
   drawn by `::after`, ringed in the paper colour so it reads at 3:1 against
   both the page and dark text (WCAG 1.4.11) in either theme.

   Nothing here is direction-aware: `touch_selection.mjs` orders the two
   endpoints by DOCUMENT position and places each handle on its caret rect, so
   in a right-to-left paragraph the start handle is on the right, where it
   belongs, without a rule in this file knowing about it. */
.overlay .touch-handle {
  position: absolute;
  pointer-events: auto;
  touch-action: none;
  cursor: grab;
  display: grid;
  place-items: center;
  background: transparent;
}

.overlay .touch-handle::after {
  content: "";
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--paper);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
}

/* The loupe. Fixed to the window rather than to a page, because staying inside
   the WINDOW is its whole placement rule (`magnifierPlacement` flips it below
   the finger at the top of the screen rather than letting it paint off-window).
   `pointer-events: none` so it never eats the drag it is describing. */
.touch-magnifier {
  position: fixed;
  width: 100px;
  height: 100px;
  border-radius: 50%;
  overflow: hidden;
  z-index: var(--z-magnifier);
  pointer-events: none;
  background: var(--paper);
  border: 1px solid var(--line-strong);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
}

.touch-magnifier > canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* The table GUTTER (docs/141 D-2): the row and column grab zones, the highlight
   on the table's own leading border, and the `+` insert disc on a boundary.

   ## The border IS the affordance

   The owner, on the previous cut: *"instead of adding new header on left and
   top.. why not highlighting the existing border.. and using that"*. So there is
   no bar beside the table any more. What is drawn is a 3px line **centred on the
   table's own leading border**, at that border's geometry, in three states.

   **ONLYOFFICE is the source that was actually read** (Google Docs could not be
   checked from this machine, so it is not cited):
   `CTable.prototype.private_CheckHitInBorder` finds a border within ±3px and
   then CANCELS it — `Border = -1` — on the leading edges, reporting
   `RowSelection` / `ColumnSelection` instead. The leading border selects; the
   inner borders resize. Ours cannot collide even by accident: the engine reports
   one boundary per band at its CLOSING edge, so the leading top and left borders
   carry no resize zone at all. `table_gutter_zones.mjs` has the quotation.

   ## Drawn 3px, targeted 14px (24 under a finger)

   The grab zone keeps every pixel it had and gains a 3px reach past the border,
   so the border sits inside the target rather than one pixel outside it. It
   paints nothing itself. This is the separation the resize grips already make —
   9px drawn inside a 24px WCAG 2.5.8 hit area — and it means a finger's larger
   target changes no pixel of what is on screen.

   ## Translucent at rest, on purpose

   The rest and hover fills are `--paper-ink` mixed into TRANSPARENT rather than
   into the paper, because this line is drawn ON TOP of a border the engine has
   already painted. An opaque grey would bleach a black hairline border into
   grey — it would remove the thing it is meant to be highlighting. Mixed into
   transparent it darkens what is under it, and on a table whose border is `none`
   it is still the only visible mark, which is what keeps a borderless table
   findable.

   No new token: `--paper-ink` and `--paper` are the paper layer, which is
   theme-independent because the sheet is white in BOTH themes, and `--accent` is
   the fill the cell selection already uses.

   `cursor: cell` is the CSS keyword whose meaning is "select a table cell", and
   it is what `pointer_cursor.mjs` row `table-row-strip` promises. */
.overlay .table-row-strip,
.overlay .table-column-strip {
  position: absolute;
  pointer-events: auto;
  touch-action: none;
  cursor: cell;
  background: transparent;
}

/* The three states of the border highlight. One geometry — the line the painter
   centres on the border — and only the colour changes between them, so a band
   never moves as it goes from resting to hovered to selected. */
.overlay .table-gutter-edge,
.overlay .table-gutter-hover,
.overlay .table-gutter-selection {
  position: absolute;
  pointer-events: none;
  border-radius: 1px;
}

/* Rest: the pointer is in the table, so both leading borders say they can be
   grabbed — quietly, and translucently so the border beneath still reads. */
.overlay .table-gutter-edge {
  background: color-mix(in srgb, var(--paper-ink) 26%, transparent);
}

/* Under the pointer: this band's stretch of that border, clearly darker. */
.overlay .table-gutter-hover {
  background: color-mix(in srgb, var(--paper-ink) 62%, transparent);
}

/* Selected — and only now — accent, and opaque: a selection is not a hint. */
.overlay .table-gutter-selection,
.overlay .table-gutter-hover[data-selected] {
  background: var(--accent);
}

/* The reorder GRIP: a transparent slab the width of the grab zone, covering the
   band whose row or column is already selected. It carries the cursor and the
   press, because a `grab` on the 3px line would be a promise on a target too
   thin to find. Nothing is painted here — the highlight above is what is seen. */
.overlay .table-gutter-band {
  position: absolute;
  pointer-events: none;
  background: transparent;
}

/* The same band once it IS the selection: now a handle. Docs reorders a row by
   dragging a strip that is already selected, and `grab` is the only thing that
   says so before the press. Opting back into pointer events is what makes the
   cursor reachable at all — the band is a child of the overlay, which is
   `pointer-events: none`. */
.overlay .table-gutter-band[data-move="ready"] {
  pointer-events: auto;
  touch-action: none;
  cursor: grab;
}

/* Where a reorder would drop, tracked live. A 2px accent rule across the table
   at the boundary — Docs' indicator — drawn OUTSIDE the flow with a transform so
   a zero-height box still paints, and never a hit target of its own. */
.overlay .table-move-indicator {
  position: absolute;
  pointer-events: none;
  background: var(--accent);
  border-radius: 1px;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--surface) 70%, transparent);
}

.overlay .table-move-indicator[data-axis="row"] {
  height: 2px;
  transform: translateY(-1px);
}

.overlay .table-move-indicator[data-axis="column"] {
  width: 2px;
  transform: translateX(-1px);
}

/* A 16px disc centred on the boundary, inside the strip. Solid accent on the
   surface colour is the pairing the AA contrast sweep already covers, and the
   glyph is a plain `+` rather than an icon font so it needs no ligature and no
   translation. */
.overlay .table-insert-target {
  position: absolute;
  pointer-events: auto;
  touch-action: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  color: var(--surface);
  background: var(--accent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--surface) 85%, transparent);
}

/* Checklist checkbox click target (docs/67 — checklist authoring). The checkbox
   glyph is painted in the page raster; this transparent, clickable overlay sits
   over it in the hanging gutter so a click toggles the item. A faint hover ring
   signals it is interactive without competing with the drawn glyph. */
.overlay .checklist-marker {
  position: absolute;
  cursor: pointer;
  pointer-events: auto;
  border-radius: 4px;
  touch-action: none;
}

.overlay .checklist-marker:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
}

.overlay .caret {
  position: absolute;
  background: var(--paper-ink);
  animation: caret-blink 1.06s steps(1) infinite;
}

/* Live IME composition preview: shows in-progress text at the caret without
 * ever touching the document — compositionend still owns the real commit. */
/* The editable focus owner (docs/105 UX-001).
 *
 * `#pages` paints pixels and cannot own text input: a non-editable element
 * raises no soft keyboard on touch and fires no composition events, so IME,
 * dictation and platform text services were unreachable. This textarea holds
 * focus instead and is moved to the caret on every repaint.
 *
 * Every rule here is load-bearing — the obvious ways to hide an element all
 * break it:
 *   - `display:none` / `visibility:hidden` / `width:0` cannot receive focus at
 *     all, and iOS will not raise the keyboard for them. It stays a real, 1px,
 *     transparent, focusable box.
 *   - it must sit AT the caret, not off-screen: an IME candidate window and the
 *     iOS autocorrect bar anchor to the focused element's box, so parking it at
 *     -10000px puts the candidate list in the wrong place (or off-screen).
 *   - `font-size` must be >= 16px or iOS Safari zooms the whole page on focus
 *     (the same rule the coarse-pointer block applies to real inputs).
 *   - the engine paints the caret, so this element's own caret is hidden. */
.editor-text-input {
  position: fixed;
  top: 0;
  left: 0;
  width: 1px;
  height: 1em;
  min-height: 0;
  padding: 0;
  margin: 0;
  border: 0;
  outline: none;
  opacity: 0;
  resize: none;
  overflow: hidden;
  white-space: pre;
  font-size: 16px;
  line-height: 1;
  color: transparent;
  background: transparent;
  caret-color: transparent;
  /* Below the page chrome: it is never seen, and must never take a pointer
   * event away from the surface it represents. */
  z-index: 0;
  pointer-events: none;
}

/* ---- Chrome modes: compact vs ribbon --------------------------------------
 * Two mutually exclusive toolbars. `body` carries exactly one of
 * `.compact-mode` / `.ribbon-mode`, so neither can be half-shown. */
body.compact-mode .ribbon { display: none; }
/* The tab strip is no longer inside `.ribbon` — it sits in the header, where
 * the compact chrome puts its menu bar (docs/123 §3) — so hiding the ribbon no
 * longer hides it. It is named here explicitly, next to the rule it used to
 * inherit from, so the pair cannot drift apart. */
body.compact-mode .ribbon-nav { display: none; }
body.ribbon-mode .compact-toolbar { display: none; }

/* The header's centre column is a fixed 500px in compact chrome, which is all
 * a 7-name menu bar needs. The tab strip is wider than that — 8 tabs plus the
 * chevron — so in ribbon chrome the column takes the room the controls leave. */
body.ribbon-mode .document-chrome { max-width: none; }

/* Ribbon chrome stacks a 28px tab strip under the title inside the SAME 58px
 * header the compact chrome fills with a 21px menu bar, so the title row takes
 * Google Docs' own 24px (measured in their editor at 1408px: title input top
 * 10, height 24) and the header's vertical padding tightens to 3px.
 * 3 + 24 + 28 + 3 = 58 exactly, so the header height stays one number for both
 * chromes instead of becoming two that drift. Scoped to ribbon mode, because
 * the compact header was measured against Docs in #591 and is not in scope. */
body.ribbon-mode .bar {
  padding-top: 3px;
  padding-bottom: 3px;
}

body.ribbon-mode .document-title-row {
  height: 24px;
}

body.ribbon-mode .doc-title {
  padding-top: 1px;
  padding-bottom: 1px;
}

/* ONE navigation axis per chrome (`109` UX-014, docs/122). The editor used to
 * show a menu bar AND a ribbon tab strip at the same time, so finding a command
 * meant first guessing which of two systems owned it.
 *
 * Ribbon mode's axis is the tab strip, whose first tab is File — ONLYOFFICE's
 * arrangement, and they ship no menu bar at all (`template/Viewport.template`
 * has no menubar element; their locale has no Edit/Format/Tools menu strings).
 * Compact mode has no band to hang a File page under, so its axis is the menu
 * bar, which is what Google Docs shows above a single flat toolbar.
 *
 * Hidden with `display: none` rather than removed from the markup because the
 * bar is one chrome's real navigation, not dead weight: `setChromeMode` switches
 * which axis is live, and the compact chrome needs its buttons in the DOM. */
body.ribbon-mode #appMenuBar { display: none; }

/* …with ONE exception, and it is not a hedge. Before a document is open the
 * empty-state block below hides the whole `.ribbon` — bands of disabled controls
 * around a drop card read as a broken shell — which takes the tab strip and its
 * File tab with it. Hiding the bar as well would leave a freshly loaded editor
 * with NO route to Open at all: the drop card and nothing else. So the bar is the
 * empty state's axis in both chromes, and there is still only ever one on screen,
 * because the band it would compete with is not there. ONLYOFFICE does the same
 * thing from the other side: their read-only viewport declares the File tab and
 * nothing else (`Toolbar.js:1885`). */
body.ribbon-mode:not(.doc-loaded) #appMenuBar { display: flex; }

/* The File tab: a page, not a band. Set apart from the command tabs the way
 * ONLYOFFICE sets theirs apart (`x-lone`) and Word tints its File tab, so it
 * does not read as one more band to browse. */
.ribbon-tab-file {
  margin-right: 6px;
}

.ribbon-tab-file[aria-selected="true"] {
  background: var(--bg-2);
  border-radius: var(--radius) var(--radius) 0 0;
}

/* Contextual tabs sit at the right-hand end and say so: a permanent-looking tab
 * that happens to be greyed out is a different message from a tab that appears
 * when its object does. */
.ribbon-tab-contextual {
  margin-left: 6px;
}

/* The File page COVERS the work area — it is a route, not an overlay.
 *
 * It used to anchor to `--chrome-bottom`, which is where the chrome ends when
 * a BAND is showing. On the File tab there is no band: the page replaces it.
 * So the page began 74px below the chrome's real bottom edge and the ruler,
 * the nav rail and the top of the sheet stayed on screen above it — a
 * full-window route that read as a half-open drawer. It anchors to the
 * navigation row's bottom edge instead, which is the header's, and that is
 * also what ONLYOFFICE does: `Viewport.js:182` sets their page's `top` to the
 * tab strip's height and `.toolbar-fullview-panel` (`toolbar.less:940`) is
 * `position: absolute; bottom: 0; width: 100%` — full width, down to and
 * including the status bar. `file-page-coverage.spec.mjs` asserts it from
 * geometry, so it cannot half-open again.
 *
 * `z-index: 6` clears the rail (4), the ruler (4) and the footer (2) and stays
 * under the `--z-*` ladder, which is for surfaces that float over a document
 * rather than replace it. */
.ribbon-panel.file-page {
  position: fixed;
  inset: var(--h-header) 0 0 0;
  z-index: 6;
  margin: 0;
  border-radius: 0;
  padding: 0;
  /* Each column scrolls itself, which is what keeps the rail still while the
     pane moves — ONLYOFFICE's `.content-box` is `height: 100%; overflow:
     hidden` inside a full-height `.panel-context` for the same reason. */
  overflow: hidden;
  /* The CONTENT side of the page, and it is the light surface — ONLYOFFICE's
     `.panel-context` is `@background-normal` while the rail beside it is
     `@background-pane`. Two shades, so the column reads as one side of a page
     rather than as a list floating in a void. */
  background: var(--surface);
  /* Two columns, as theirs is: the rail, then the pane. `flex` rather than the
     `padding-left: 260px` they use, because our rail's width is content-derived
     (320px, see below) rather than a constant they can hard-code twice. */
  display: flex;
  align-items: stretch;
}

/* `display: block` above is as specific as `.ribbon-panel[hidden]`, and it comes
 * later, so without this the hidden File page stayed on screen swallowing every
 * click on the document beneath it. Restated rather than reordered: a reader of
 * either rule can see the other is accounted for. */
.ribbon-panel.file-page[hidden] {
  display: none;
}

/* One LEFT column, at the window's own gutter — not a 720px block centred in
 * the window, which left a 344px dead margin before the first word.
 *
 * ONLYOFFICE's page is two columns: a 260px navigation rail
 * (`#file-menu-panel .panel-menu`) and a content pane that shows the selected
 * category (`filemenu.less:2-12`, `width:100%` + `padding-left: 260px`). We
 * This is that rail, at their density, beside that pane. What the pane shows
 * is their fallback rather than a selected category: `FileMenu.js:390` opens
 * on Save-As when the document can be downloaded and on **Info** otherwise,
 * and Info is the one pane whose content we already have. Turning Print,
 * Settings and Save-As into panes of their own is `109` UX-026.
 *
 * 320px because the rows are menu rows carrying a chord on the right, and
 * Google Docs' File menu — the SAME roster, rendered as the compact chrome's
 * dropdown — measures 322px wide with the same content. 260px ellipsises
 * "Recover unsaved work ⌘⇧D". */
.file-page-inner {
  flex: 0 0 min(320px, 100%);
  margin: 0;
  padding: 12px 14px 24px;
  overflow-y: auto;
  /* ONLYOFFICE's rail: `@background-pane` with a one-pixel right border
     (`#file-menu-panel .panel-menu`). */
  background: var(--bg);
  border-right: 1px solid var(--line);
  overscroll-behavior: contain;
}

.file-page-body {
  display: flex;
  flex-direction: column;
}

/* The content pane. ONLYOFFICE's is `.content-box { height: 100%; padding: 0
 * 20px; overflow: hidden }` inside a `width: 100%` context; the 20px gutter is
 * theirs, the rest follows our own type scale. */
.file-page-detail {
  flex: 1 1 auto;
  min-width: 0;
  padding: 20px 24px;
  overflow: auto;
  overscroll-behavior: contain;
}

/* The heading takes the SAME measure as the content it heads — see
 * `--file-page-measure`. It was uncapped, so at 1920 a one-line title was
 * allowed 1552px of pane over a 760px column. `file-page-panes.spec.mjs`
 * measures the three right edges against each other. */
.file-page-detail h2 {
  margin: 0 0 4px;
  max-width: var(--file-page-measure);
  font-size: 20px;
  font-weight: 600;
  color: var(--ink);
}

.file-page-detail .file-detail-sub {
  margin: 0 0 20px;
  max-width: var(--file-page-measure);
  color: var(--muted);
}

/* A definition list, because that is what it is: a label and its value. Two
 * columns so the values line up, which is how their Info panel reads. */
.file-page-detail dl {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 8px 24px;
  margin: 0;
  max-width: 520px;
}

.file-page-detail dt {
  color: var(--muted);
}

.file-page-detail dd {
  margin: 0;
  color: var(--ink);
}

/* The rule under Back, and ONLYOFFICE's `li.devider-small` margins (7px above,
 * 11px below) — the gap that separates "leave" from "do something". */
.file-page-rule {
  height: 1px;
  margin: 7px 0 11px;
  border: 0;
  background: var(--line);
}

.file-page-back .ms {
  margin-right: 8px;
  font-size: var(--icon-control);
  vertical-align: -4px;
}

.file-page-item-heading {
  margin: 10px 0 2px;
  padding: 0 8px;
  font-size: var(--fs-caption);
  font-weight: 650;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The FIRST band's heading, not every band's. A heading is now the first child
 * of its own `role="group"` wrapper, so `:first-child` alone matched all of
 * them and collapsed the space between every band on the File page. */
.file-page-item-group:first-child .file-page-item-heading {
  margin-top: 0;
}

/* Row metrics are ONLYOFFICE's File rail, measured in their shipped 9.4.1
 * stylesheet and confirmed in their live editor: `li.fm-btn` is 28px tall with
 * 4px below it — a 32px pitch — at 12px type. Ours were 34px tall on a 36px
 * pitch, which is 12% fewer rows on a screen for no gain in legibility; Google
 * Docs' File menu is likewise 32px. Type stays at our own `--fs-body` (13px)
 * rather than their 12px: this stylesheet has one body size and a second one
 * here would be a fork, not a decision. */
.file-page-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  width: 100%;
  min-height: 28px;
  margin-bottom: 4px;
  padding: 2px 8px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.file-page-item:hover:not(:disabled),
.file-page-item:focus-visible {
  background: var(--bg-2);
}

.file-page-item:disabled {
  color: var(--muted);
  cursor: default;
}

.file-page-item-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.file-page-item-hint {
  flex: 0 0 auto;
  color: var(--muted);
  font-size: var(--fs-caption);
}

.compact-toolbar {
  /* The raised pill within the grey field — lighter than the chrome behind it,
     so it separates by SHADE with no border, exactly as Docs and the sister app
     draw it (`rounded-full bg-[--doc-bg-subtle] mx-2 mb-1`). */
  background: var(--surface);
  border-radius: var(--chrome-card-radius);
  margin: var(--chrome-card-inset-y) var(--chrome-card-inset-x)
    calc(var(--chrome-card-inset-y) + 4px);
  display: flex;
  align-items: center;
  /* The gap sits BETWEEN groups; inside a group the controls are flush, which
     is what makes a group read as one thing (Google Docs draws its toolbar the
     same way). It used to be 2px between all 34 children — 66px of the bar
     spent separating controls that belong together. */
  gap: 4px;
  padding: 3px 8px;
  min-height: 40px;
  /* NOT `auto`. A toolbar that scrolls sideways hides controls behind a
     gesture nobody makes, and it hid them at 1280px and 1366px — every
     ordinary laptop. What does not fit folds into the "⋯" menu instead
     (`compact_toolbar.mjs`), so there is never anything to scroll to. */
  overflow: hidden;
}

/* A group of related controls: flush inside, separated outside. */
.compact-toolbar .cgroup {
  display: inline-flex;
  align-items: center;
  gap: 0;
  flex: 0 0 auto;
}
/* A CONTEXTUAL group that is not showing. `display: inline-flex` above beats the
   `hidden` attribute's own `display: none`, so without this the Table group kept
   its 48px while invisible — the same shape as the two placeholders that ignored
   `[hidden]` in the embed chrome. */
.compact-toolbar .cgroup[hidden] {
  display: none;
}

/* The seven boundaries Docs draws a rule at. The rest read as a gap. */
.compact-toolbar .cgroup[data-divider]:not(:first-child) {
  border-left: 1px solid var(--line);
  margin-left: 2px;
  padding-left: 4px;
}

/* One control size for the whole bar, so the rhythm does not drift the way
 * ad-hoc per-button sizes did. 34px matches the prototype's `.tool`. */
.compact-toolbar .ctool {
  min-width: 34px;
  height: 34px;
  padding: 0 7px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-2, var(--ink));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  flex: 0 0 auto;
}
.compact-toolbar .ctool:hover:not(:disabled) { background: var(--hover, color-mix(in srgb, var(--ink) 8%, transparent)); }
.compact-toolbar .ctool:disabled { opacity: 0.38; cursor: default; }
.compact-toolbar .ctool[aria-pressed="true"] {
  background: var(--accent-soft);
  color: var(--accent-text);
}
/* 3:1 non-text focus indicator, not a tint (WCAG 1.4.11). */
.compact-toolbar .ctool:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: -2px;
}
.compact-toolbar .ctool .ms { font-size: 20px; line-height: 1; }
.compact-toolbar .cadopted {
  height: 30px;
  flex: 0 0 auto;
  align-self: center;
}
/* Adopted combos read as Docs-style borderless controls in the compact bar —
 * the ribbon draws them with a box, which is right in a grouped band and wrong
 * in a single dense strip. Border appears on hover/focus, as Docs does. */
.compact-toolbar .cadopted,
.compact-toolbar .cadopted:not(:hover):not(:focus-visible) {
  border-color: transparent;
  background: transparent;
  box-shadow: none;
}
.compact-toolbar .cadopted:hover {
  background: var(--hover, color-mix(in srgb, var(--ink) 8%, transparent));
  border-color: transparent;
}
.compact-toolbar .cadopted:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: -2px;
  border-color: transparent;
}
/* The Styles trigger is the SAME element the ribbon owns, adopted rather than cloned
   (docs/115 §5.6). The compact bar is one dense 30px row and much narrower than the
   band, so the trigger narrows here and lets its label ellipsise — the `title` and
   the open menu both still carry the whole name. It keeps its own border, because a
   borderless control is not readable as a dropdown. */
.compact-toolbar .styles-trigger.cadopted {
  /* 140px, not the 227px an inline `min-width` in the layout table used to
     force on top of this rule — 95px of the bar spent on a control that had
     already been designed narrower here. 140px is the smallest width at which
     the longest style a default document offers as a HEADING still reads in
     full ("Heading 1" measures 61px in the label box this leaves). Longer
     names ("Header and Footer", 115px) ellipsise, and the `title` and the open
     menu both still carry the whole name — the same trade docs/115 records for
     the font trigger. */
  width: 140px;
  height: 30px;
}
.compact-toolbar .styles-trigger.cadopted:not(:hover):not(:focus-visible) {
  border-color: var(--line-strong);
}
/* The font trigger was 14px WIDER here than in the ribbon, for no reason: the
   ribbon's authored width is 122px (docs/64's budget note is on that rule) and
   the ellipsis behaviour is identical at both widths. */
.compact-toolbar .font-family-trigger.cadopted {
  width: 122px;
  min-width: 122px;
}
/* The size box loses its spinner here and keeps only the value. This bar puts
   Docs' − and + buttons on either side of it, so the spinner is a third way to
   do the same thing, 14px wide. 58px still shows 1638 — the largest size the
   format allows — without clipping. */
.compact-toolbar #fontSize.cadopted {
  width: 58px;
  min-width: 58px;
  appearance: textfield;
  -moz-appearance: textfield;
}
.compact-toolbar #fontSize.cadopted::-webkit-inner-spin-button,
.compact-toolbar #fontSize.cadopted::-webkit-outer-spin-button {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
}

/* A control that opens a menu: the icon plus a small caret, as Docs draws its
   align and list buttons. The caret is part of the one button, not a second
   hit target — the whole control opens the menu. */
.compact-toolbar .ctool-menu { padding: 0 2px 0 6px; }
.compact-toolbar .ctool-menu .ctool-caret {
  font-size: 18px;
  margin-left: -2px;
  color: var(--muted);
}
.compact-toolbar .ctool-overflow { margin-left: auto; }

/* The align menu and the "⋯" menu are `.context-menu` surfaces (so they
   inherit the product's menu skin, elevation and light-dismiss) laid out as a
   row of icon buttons rather than a list of labels. */
.compact-align-menu,
.compact-overflow-menu {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 4px;
  /* `.context-menu` is sized for rows of labels; these hold icon buttons and
     must be exactly as wide as they are. */
  min-width: 0;
  width: max-content;
  overflow: visible;
}
.compact-toolbar .ctool-overflow[hidden] { display: none; }
/* The compact bar's Table dropdown. Rows of LABELS, not icon buttons, so it is
   the plain `.context-menu` shape and needs only a scroll bound: the table
   command set is 24 rows, which is taller than a short window. */
.compact-command-menu {
  max-height: min(70vh, 520px);
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* A sheet says what is ON (`compact_toolbar.mjs`'s `SHEET_STATE`). The Aa
   sheet's character toggles are Google Docs' toggle row — B / I / U / S side by
   side at the head of the band, lit when on — and they light the way the bar's
   own toggles do (`.compact-toolbar .ctool[aria-pressed="true"]`), so a toggle
   reads the same in the bar and in the sheet. Same box, same tokens. */
.compact-command-menu .menu-toggle-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 2px 4px 4px;
}
.compact-command-menu .menu-toggle {
  min-width: 34px;
  height: 34px;
  padding: 0 7px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-2, var(--ink));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  cursor: pointer;
}
.compact-command-menu .menu-toggle .ms { font-size: 20px; line-height: 1; }
.compact-command-menu .menu-toggle:hover:not(:disabled) {
  background: var(--hover, color-mix(in srgb, var(--ink) 8%, transparent));
}
.compact-command-menu .menu-toggle:disabled { opacity: 0.38; cursor: default; }
.compact-command-menu .menu-toggle[aria-checked="true"] {
  background: var(--accent-soft);
  color: var(--accent-text);
}
/* Half the selection is bold: outlined rather than filled, so "mixed" is
   neither of the two answers it is not. */
.compact-command-menu .menu-toggle[aria-checked="mixed"] {
  box-shadow: inset 0 0 0 1px var(--accent-text);
}
.compact-command-menu .menu-toggle:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: -2px;
}
/* Rows with a state (alignment, lists) carry the chrome's one tick, the
   language and spacing menus' `.menu-check`, opacity-toggled. */
.compact-command-menu .menu-item[role="menuitemcheckbox"],
.compact-command-menu .menu-item[role="menuitemradio"] {
  display: flex;
  align-items: center;
  gap: 8px;
}
.compact-command-menu .menu-item[aria-checked="true"] .menu-check {
  opacity: 1;
}
.compact-command-menu .menu-item[aria-checked="mixed"] .menu-check {
  opacity: 0.45;
}
.compact-overflow-menu {
  flex-wrap: wrap;
  max-width: min(560px, calc(100vw - 24px));
  row-gap: 4px;
}
.compact-align-menu .ctool,
.compact-overflow-menu .ctool {
  min-width: 34px;
  height: 34px;
  padding: 0 7px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-2, var(--ink));
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  flex: 0 0 auto;
}
.compact-align-menu .ctool .ms,
.compact-overflow-menu .ctool .ms { font-size: 20px; line-height: 1; }
.compact-align-menu .ctool:hover:not(:disabled),
.compact-overflow-menu .ctool:hover:not(:disabled) {
  background: var(--hover, color-mix(in srgb, var(--ink) 8%, transparent));
}
.compact-align-menu .ctool:disabled,
.compact-overflow-menu .ctool:disabled { opacity: 0.38; cursor: default; }
.compact-align-menu .ctool[aria-checked="true"],
.compact-overflow-menu .ctool[aria-pressed="true"] {
  background: var(--accent-soft);
  color: var(--accent-text);
}
.compact-align-menu .ctool:focus-visible,
.compact-overflow-menu .ctool:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: -2px;
}
/* A folded group keeps its grouping inside the menu, so the reading order the
   bar had is the reading order the menu has. */
.compact-overflow-menu .cgroup {
  display: inline-flex;
  align-items: center;
  gap: 0;
  flex: 0 0 auto;
}
.compact-overflow-menu .cgroup:not(:first-child) {
  border-left: 1px solid var(--line);
  margin-left: 2px;
  padding-left: 6px;
}
.compact-overflow-menu .cadopted { height: 30px; flex: 0 0 auto; }

/* The editing-mode selector at the top right (`header_mode.mjs`, `109` UX-025).
   Google Docs' "Editing ▾": one button naming the mode in force. It carries the
   mode's own colour only for Suggesting — the review-insert green the status
   bar's pressed segment already uses — because that is the mode a person must
   not forget they are in; Editing and Read only are drawn neutral. */
.header-mode {
  display: inline-flex;
  align-items: center;
}

.header-mode-btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 26px;
  padding: 0 4px 0 8px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-ribbon-caption);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}

.header-mode-btn .ms {
  font-size: var(--icon-compact);
}

.header-mode-btn:hover,
.header-mode-btn[aria-expanded="true"] {
  background: var(--bg-2);
}

.header-mode-btn:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

.header-mode-btn[data-mode="suggesting"] {
  color: var(--review-insert);
  border-color: var(--review-insert-line);
  background: var(--review-insert-soft);
}

.header-mode-menu {
  min-width: 248px;
}

.header-mode-item {
  grid-template-columns: 18px minmax(0, 1fr);
  align-items: start;
  padding-block: 7px;
}

.header-mode-item .ms {
  font-size: var(--icon-control);
}

.header-mode-item-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.header-mode-item-label {
  font-weight: 600;
}

.header-mode-item-note {
  color: var(--muted);
  font-size: var(--fs-small);
  white-space: normal;
}

.header-mode-item[aria-checked="true"] {
  background: var(--accent-soft);
  color: var(--accent-text);
}

.header-mode-item[aria-disabled="true"] {
  opacity: 0.45;
  cursor: default;
}

/* Before a document is open the mode means nothing — the status bar that carries
   the other face is hidden then too — and on a phone the status bar's own
   segments are the one face: the two-row phone header has no room for a third
   cluster, and a control repeated in a 390px viewport is the clutter the phone
   tier exists to remove. */
body:not(.doc-loaded) .header-mode,
body.phone-mode .header-mode {
  display: none;
}

/* Below 1000px the mode's NAME goes and its glyph stays, which is what Google
   Docs does with the same control in a narrow window. Measured, the narrowest
   window in which every ribbon tab fits without the strip scrolling: 868px before
   the selector existed, 968px with its name shown, 928px with the glyph alone —
   so the selector costs the strip 60px at the narrow end, and nothing at 1024px
   or above. Below the fit the strip scrolls under its fade, as it always has, and
   every tab stays reachable by scroll, by arrows and by key tips. The name is
   still the button's accessible name — it is
   referenced by `aria-labelledby`, which reads hidden text — and its title. */
@media (max-width: 1000px) {
  .header-mode-text {
    display: none;
  }
}

/* Header mode switch */
.chrome-mode {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  border-radius: 999px;
  background: var(--surface-2, color-mix(in srgb, var(--ink) 6%, transparent));
  margin-right: 8px;
}
.chrome-mode-btn {
  height: 26px;
  padding: 0 11px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-2, var(--ink));
  font-size: var(--fs-caption, 11px);
  font-weight: 600;
}
.chrome-mode-btn[aria-checked="true"] {
  /* Selected must be unmistakable: the accent fill, not a white chip whose only
     cue was a 1px shadow on an already-light field. */
  background: var(--accent);
  color: var(--accent-ink);
  box-shadow: none;
}
.chrome-mode-btn[aria-checked="false"]:hover {
  background: color-mix(in srgb, var(--ink) 8%, transparent);
}
.chrome-mode-btn:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

/* Touch: the same 44px floor the coarse-pointer block applies elsewhere. */
@media (pointer: coarse) {
  .compact-toolbar .ctool { min-width: 44px; height: 44px; }
  /* The sheet's toggle row floats over the document, so it takes the same
     floor as the bar it opened from; six of them are 276px of a 320px sheet. */
  .compact-command-menu .menu-toggle { min-width: 44px; height: 44px; }
  .chrome-mode-btn { height: 36px; padding: 0 14px; }
}

.overlay .ime-preedit {
  position: absolute;
  display: flex;
  align-items: center;
  padding: 0 2px;
  color: var(--paper-ink);
  background: color-mix(in srgb, var(--accent) 10%, var(--paper));
  border-bottom: 2px solid var(--accent);
  white-space: pre;
  font: inherit;
}

/* Active table-cell outline: a soft accent ring so you see the cell you edit. */
.overlay .cell-outline {
  position: absolute;
  border: 1.5px solid var(--accent);
  border-radius: 2px;
  box-shadow: 0 0 0 3px var(--accent-soft);
  background: color-mix(in srgb, var(--accent) 5%, transparent);
}

@keyframes caret-blink {
  50% {
    opacity: 0;
  }
}

/* ---- Empty / drop state ----------------------------------------------------
 * Before a document is open, the ribbon/toolbar, nav rail, and footer status
 * bar have nothing to act on — showing them disabled around a small drop
 * card read as a broken app shell. `body:not(.doc-loaded)` hides that chrome
 * entirely; only the minimal top bar (brand + Open + Settings) remains, and
 * the drop card becomes the whole page. `openBytes` adds `.doc-loaded` to
 * `<body>` once a document is open, which reveals the chrome again. */
body:not(.doc-loaded) .ribbon,
body:not(.doc-loaded) .ribbon-nav,
body:not(.doc-loaded) .rail,
body:not(.doc-loaded) .footer,
body:not(.doc-loaded) .search-trigger,
body:not(.doc-loaded) #save,
body:not(.doc-loaded) .save-format,
body:not(.doc-loaded) .compatibility-status,
/* The state chip says what state THE DOCUMENT is in, and before one exists there
   is no state to report: the markup's resting text is "Opened", so the boot frame
   and the bare `?blank=1` upload screen both claimed a document had been opened
   over an empty drop card. Hidden with its neighbour until `.doc-loaded`. */
body:not(.doc-loaded) .document-state,
body:not(.doc-loaded) #propertiesBtn {
  display: none;
}

body:not(.doc-loaded) .drop {
  min-height: calc(100vh - var(--h-header));
}

.drop {
  display: grid;
  place-items: center;
  min-height: 60vh;
}

.drop[hidden] {
  display: none;
}

.drop-card {
  text-align: center;
  max-width: 420px;
  padding: 40px 32px;
}

body:not(.doc-loaded) .drop-card {
  max-width: 480px;
  padding: 56px 40px;
}

.drop-icon {
  color: var(--faint);
  font-size: 44px;
  margin-bottom: 8px;
}

body:not(.doc-loaded) .drop-icon {
  font-size: 56px;
}

.drop-title {
  font-size: 17px;
  font-weight: 600;
  margin: 4px 0 6px;
}

body:not(.doc-loaded) .drop-title {
  font-size: 20px;
}

.muted {
  color: var(--muted);
}

/* ---- Footer status bar ----------------------------------------------------- */
/* Word and Docs both treat the status bar as a fixed-height strip that never
   wraps, never scrolls and never gives the document a horizontal scrollbar: as
   the window narrows they *shed* the lowest-value indicators. We reproduce that
   with the `[data-status-priority]` ladder further down, so a new indicator
   inherits the behaviour by declaring a number instead of by earning its own
   breakpoint. The strip deliberately declares no `overflow` of its own: an
   earlier revision added `overflow-x: clip` here as a "backstop", but clip does
   not propagate to `documentElement.scrollWidth` — measured on this page, a
   400px overspill forced into `.foot-right` at a 720px viewport reports
   `.footer.scrollWidth` 861 while `documentElement.scrollWidth` stays 720. Since
   the shell-overflow spec asserts `documentElement` and `.bar`, that backstop
   silenced the very assertion that caught this bar overflowing in the first
   place. Getting the ladder wrong must stay visible, so the clip is gone and the
   spec now measures `.foot-right` against its own box as well. */
.footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  height: var(--h-footer);
  padding: 0 12px 0 16px;
  font-size: var(--fs-small);
  color: var(--muted);
  border-top: 0;
  background: var(--bg);
  z-index: 2;
  min-width: 0;
}

.foot-left,
.foot-right {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

/* The two halves are deliberately asymmetric, mirroring `.document-chrome` /
   `.controls` in the header. The left half is informational text, so it is the
   one that yields and truncates; the right half is live controls (mode, page
   position, zoom), and a half-clipped button is worse than no truncation at
   all, so it keeps its intrinsic width. Without this split both halves shrank
   below min-content and their `white-space: nowrap` contents painted outside
   the footer box — shrinking the boxes moved the overflow, it never removed it. */
.foot-left {
  flex: 1 1 auto;
  overflow: hidden;
}

.foot-right {
  flex: 0 0 auto;
}

.footer-mode {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  white-space: nowrap;
}

.footer .review-mode-seg {
  padding: 2px 8px;
  font-size: var(--fs-ribbon-caption);
}

.rgroup-mode .review-mode-seg {
  padding: 5px;
}

.rgroup-mode .review-mode-seg .ms {
  display: none;
}

/* Inside the left half the transient engine message comes FIRST. It used to be
   the only member allowed to give way, on the argument that "12,483 words" is
   worthless the moment a digit is cut — and that argument is right about the
   digits and wrong about the priority: a message is the thing the reader is
   being told NOW ("Could not fetch the dictionary", "Draft saved"), the counts
   are on screen the rest of the time, and measured at 1280px the message got
   0-106px and read "Cou…", "Fetc…", "Draf…" while the counts kept all of theirs.
   So the message takes its whole width, up to the whole half; the counts give
   way — a WHOLE count at a time, never a cut digit (`.stats` below) — and the
   message only ellipsizes when it is longer than the half itself, in which case
   `status_line.mjs` puts the full text on its `title`. */
.status {
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 0 0 auto;
  max-width: 100%;
  min-width: 0;
}

.status.error {
  color: var(--danger);
  font-weight: 600;
}

/* --- The toast (`109` UX-017) ------------------------------------------------
   Google Docs' snackbar shape: pinned to the VIEWPORT, bottom-left, above the
   footer. Pinned to the viewport rather than to the footer is the point — the
   footer's informational half is `display: none` below 620px, which is how the
   editor came to refuse an edit silently and inaudibly at phone widths.

   `pointer-events: none` and nothing focusable inside, so the card can sit over
   the document and over a modal without ever stealing a click or a Tab stop.
   The announcement is the body-level live region's job, not this element's; this
   is `aria-hidden` so a refusal is spoken once. */
.toast {
  position: fixed;
  /* Logical, not `left`: the editor sets `documentElement.dir` from the active
     locale (`src/localize.mjs:95`), and a snackbar belongs on the reading-start
     side in Arabic and Hebrew as much as in English. */
  inset-inline-start: var(--space-3);
  bottom: calc(var(--h-footer) + var(--space-3));
  z-index: var(--z-toast);
  max-width: min(420px, calc(100vw - 2 * var(--space-3)));
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-lg);
  font-size: var(--fs-small);
  pointer-events: none;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 120ms ease-out, transform 120ms ease-out;
}

.toast.is-shown {
  opacity: 1;
  transform: translateY(0);
}

/* A refusal is marked by a bar rather than by coloured body text: the reason is
   often a whole sentence with a way out in it, and it has to stay at full
   contrast in both themes. */
.toast[data-kind="error"] {
  border-inline-start: 3px solid var(--danger-line);
  font-weight: 500;
}

/* Deliberately HERE rather than in the reduced-motion block near the top of this
   file: a media query adds no specificity, so a rule up there would be overridden
   by `.toast` down here and reduced motion would have been a comment rather than
   a behaviour. The toast still APPEARS — it is the only visible channel below
   620px, and removing it would put the silence back for anyone who asks for less
   motion. Only the slide and the fade go. */
@media (prefers-reduced-motion: reduce) {
  .toast {
    transition: none;
    transform: none;
  }
}

/* The counts yield to a message one WHOLE count at a time. The row wraps, and
   is exactly one line tall with its overflow clipped, so a count that no longer
   fits drops onto a second line nobody sees instead of being cut mid-digit —
   paragraphs first, then characters, then words, which is DOM order and is the
   `data-status-priority` order the width ladder below sheds them in too. The
   separators are `* + *::before`, so a dropped count takes its own "·" with it. */
.stats {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  align-content: flex-start;
  column-gap: 7px;
  row-gap: 0;
  height: 1.5em;
  line-height: 1.5em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
}

.stats[hidden],
.stat-pages[hidden] {
  display: none;
}

/* Separators are generated rather than authored so that shedding a count by
   priority can never leave an orphaned "·" behind, and so adding a count is a
   one-element change. Adjacency is DOM-based, so a hidden sibling carries its
   own separator out of view with it. */
.stats > * + *::before {
  content: "·";
  margin-inline-end: 7px;
  color: var(--faint);
}

.stat-pages {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Footer zoom control (Word/Docs place zoom in the status bar). */
.footer /* The language control in the status bar. A glyph plus the ENDONYM, because
   "Deutsch" tells a German reader what they will get and "DE" does not. */
.footer-language {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 6px;
  border: 0;
  border-radius: 4px;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.footer-language:hover,
.footer-language:focus-visible {
  background: var(--surface-2, var(--surface));
}

.footer-language .ms {
  font-size: 16px;
  line-height: 1;
}

.footer-language-label {
  max-width: 12ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.zoom {
  display: inline-flex;
  align-items: center;
  gap: 1px;
}

.zbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 22px;
  /* No user-agent padding: a button's default 6px a side left a 16px glyph
     12px of a 24px box, so − and + were clipped on their right edge. */
  padding: 0;
  border: none;
  border-radius: 5px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

/* Unified icon grid (doc 69 §4.2): 16px for compact/footer contexts,
   matching the other two steps (18px ribbon/menu, 20px rail) instead of a
   fourth one-off 15px size. */
.zbtn .ms {
  font-size: var(--icon-compact);
}

.zbtn:hover {
  background: var(--bg-2);
  color: var(--ink);
}

.footer .zoom select {
  height: 22px;
  border: none;
  background: transparent;
  color: var(--ink);
  border-radius: 5px;
  padding: 0 2px;
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  text-align: center;
}

.footer .zoom select:hover {
  background: var(--bg-2);
}

/* ---- Status-bar progressive disclosure ------------------------------------- */
/* Word's status bar sheds indicators as the window narrows (and lets the user
   turn individual ones off); Docs keeps only the essentials and moves counts
   into Tools ▸ Word count. Both keep the word count, the page position, the
   editing-mode affordance and zoom visible at tablet widths — so those are what
   survive here, and everything shed stays reachable from the `#stats` tooltip.
   Priorities, not element names, drive the rungs: the highest number goes
   first, and an indicator added later opts in by declaring a number rather than
   by pushing the bar over the viewport edge again.
     3 — paragraph count: not in Word's status bar at any width.
     2 — character count, and the "Mode" caption the pressed segment already
         conveys.
     1 — word count and page position: last to go, only at phone widths.
     none — engine status, the mode control and zoom: never shed.

   The rungs sit where the row genuinely runs out of room, not where it is
   convenient to test. Measured on this page with the demo document, the full
   three-count row needs 735px of footer; dropping the paragraph count brings
   that to ~635px, and the 780px condense rung below takes ~30px more. So 840
   keeps every count for as long as a realistic document's figures fit (they
   collide at ~820px), and 700 is comfortably below the 720px width the
   shell-overflow spec measures — at 720 the character count survives with
   ~115px to spare, which absorbs the ~13-23px that CI's font metrics add.
   An earlier revision shed at 1000px and 900px instead, which dropped the
   character count — shipped for Word status-bar parity — from every window
   under 900px while 78px of the strip was still empty. */
@media (max-width: 840px) {
  .footer [data-status-priority="3"] {
    display: none;
  }
}

@media (max-width: 700px) {
  .footer [data-status-priority="2"] {
    display: none;
  }

  /* …with ONE exception, and it is the whole reason this control sits in the
     status bar. Every other priority-2 item is a count — information a narrow
     window can do without. This one is the way OUT of a language you cannot
     read, and the alternative route is a dialog written in that language, so
     shedding it takes the escape hatch away exactly where it is hardest to
     replace. It survives narrowed to its glyph, which costs the row ~28px
     instead of the ~110px the endonym needs, and the list it opens names every
     language including the one in force. */
  .footer .footer-language {
    display: inline-flex;
    /* A real touch target on a row of 13px text. */
    min-width: 24px;
    min-height: 24px;
    justify-content: center;
  }

  .footer-language-label {
    display: none;
  }
}

/* Condense before clipping: the same compaction the phone tier already used,
   pulled one rung earlier so the segmented mode control keeps all three labels
   at tablet widths instead of being traded away for room. */
@media (max-width: 780px) {
  .footer {
    gap: 10px;
  }

  .foot-left,
  .foot-right {
    gap: 8px;
  }

  .footer .review-mode-seg {
    padding-inline: 5px;
    font-size: 10px;
  }
}

/* The desktop editor remains the primary target, but the header must never
   force the whole document to scroll horizontally. Collapse only redundant
   labels; the complete ribbon keeps its own horizontal scroll behavior. */
@media (max-width: 900px) {
  .bar {
    gap: 10px;
  }

  .search-trigger {
    flex: 0 0 var(--h-search);
    width: var(--h-search);
    padding: 0;
    justify-content: center;
  }

  .search-trigger .search-label,
  .search-trigger kbd {
    display: none;
  }

  #documentStateText {
    display: none;
  }

  /* The version timeline joins the established narrow-screen drawer rather than
     getting a pattern of its own — `docs/139` §14 asks for exactly that, and an
     in-flow 320px panel at 390px would leave 70px of canvas beside it, which is a
     document nobody can read next to a timeline nobody asked to fill the screen.
     As a drawer each row's ⋮ stays a 44px target with no hover involved, which
     is the touch path. */
  .side-panel.table-properties-panel,
  .side-panel.properties-panel,
  .side-panel.version-panel {
    position: fixed;
    z-index: var(--z-drawer);
    top: calc(var(--chrome-bottom) + 8px);
    right: 8px;
    bottom: calc(var(--h-footer) + 8px + var(--h-toast-band));
    width: min(340px, calc(100vw - 42px));
    max-width: none;
    margin: 0;
    box-shadow: var(--shadow-popover);
  }

  .side-panel.glyph-panel {
    position: fixed;
    z-index: var(--z-drawer);
    top: calc(var(--chrome-bottom) + 8px);
    right: 8px;
    bottom: calc(var(--h-footer) + 8px + var(--h-toast-band));
    width: min(360px, calc(100vw - 42px));
    max-width: none;
    margin: 0;
    box-shadow: var(--shadow-popover);
  }
}

@media (max-width: 620px) {
  .bar {
    gap: 8px;
    padding-inline: 8px;
  }

  .brand {
    gap: 6px;
  }

  .brand .mark {
    display: none;
  }

  .controls-doc,
  .controls-app {
    gap: 4px;
  }

  .footer {
    gap: 4px;
    padding-inline: 6px;
  }

  /* Bottom rung of the same ladder: at phone widths only the mode control and
     zoom fit, so the last counts go and the informational half goes with them.
     Everything else the footer sheds is handled by priority above. */
  .footer [data-status-priority="1"],
  .footer .foot-left {
    display: none;
  }

  .footer .foot-right {
    width: 100%;
    justify-content: space-between;
    gap: 4px;
  }

  .controls .btn {
    width: var(--h-control);
    padding: 0;
    justify-content: center;
  }

  .controls .btn-label {
    display: none;
  }

  /* The navigation rail joins the same rung: below this width it is glyphs, and
     the name is carried by the `aria-label` and the tooltip every tile already
     has (`109` HF-232). Word and Google Docs both drop a rail caption on a
     narrow window, and this is the rung where this chrome ALREADY drops a
     caption — one rule for the whole bar rather than a second breakpoint that
     only the rail knows about.
     `visibility` would keep the box; `display: none` is what actually returns
     the width, and returning the width is the point.

     WHAT THE DEFECT REALLY IS, measured rather than inherited. HF-232 was filed
     as "the captions clip — the rail reads Outlin…, Commen…, Version…", and that
     symptom is gone: the tile is `width: auto` now, so the caption no longer
     ellipsises. It grows instead, and the growth is the harm — a tile is as wide
     as the longest caption in the ACTIVE LOCALE, so the rail measures 66px in
     English, 86px in Russian and 111px in Brazilian Portuguese, which at a 320px
     viewport is 35% of the screen spent on four destinations that fit in 36px.
     Measured in Chromium at 1280/640/390/320 across en, de, ru, vi, id and
     pt-BR; the numbers are in `editor-narrow-chrome.spec.mjs`, which fails if a
     caption comes back. */
  .rail-btn > span:not(.ms) {
    display: none;
  }

  .save-format {
    max-width: 84px;
  }

  /* The compatibility count is SECONDARY: it reports what an import could not
     represent, and the reader can act on it at any width. The menu bar is the
     compact chrome's only navigation axis (`docs/122`). They share this row, so
     at phone width one of them has to give — and it cannot be the axis.

     This became reachable the day import findings started being reported at all:
     the indicator had effectively never been visible on open, so its 72px never
     competed for the row. With it visible, the bar was squeezed to width 0 —
     `display: flex` and a 0x21 box — and the compact chrome lost every menu.
     Caught by `responsive-shell.spec.mjs`, which asserts the bar signals a clip
     rather than disappearing. */
  .compatibility-status {
    display: none;
  }

  .find-panel {
    left: 8px;
    right: 8px;
    width: auto;
    max-height: calc(100vh - var(--h-header) - 96px);
  }

  .dialog-overlay {
    padding: 10px;
  }

  .dialog-card {
    max-height: calc(100vh - 20px);
    border-radius: var(--radius-dialog);
  }

  /* The narrow rung is stated in the TOKENS, not re-stated on the three
     chrome bands (HF-265 D7). `.dialog-head`, `.dialog-body` and `.dialog-foot`
     each carried `padding: 16px` here, which put every dialog below 620px back
     on hardcoded pixels — and `dialog_density.test.mjs`'s token guard could not
     see it, because its `rule()` helper anchored on `(^|\n)` plus the bare
     selector and therefore matched only the first, unindented rule in the file.
     A guard that structurally cannot read an override is not a guard; it is
     fixed now to collect EVERY rule for a selector, which is what makes this
     the only place the rung can live. */
  :root {
    --dlg-pad-x: 16px;
    --dlg-pad-y: 16px;
  }

  .dialog-heading {
    align-items: flex-start;
    gap: 10px;
  }

  .dialog-heading-icon {
    width: 34px;
    height: 34px;
    flex-basis: 34px;
    font-size: var(--icon-control);
  }

  .dialog-heading h2 {
    font-size: var(--fs-title);
  }

  .dialog-heading p {
    line-height: 1.35;
  }


  .dialog-note {
    display: none;
  }

  /* `.dialog-field-grid` is deliberately NOT collapsed here any more: forcing
     one column turned a pair of five-character fields into two 302px boxes
     stacked, which is worse on the narrowest screen, not better. Its auto-fit
     track already falls to one column when one no longer fits. */
  .properties-grid,
  .table-properties-grid,
  .metadata-facts {
    grid-template-columns: 1fr;
  }

  .field-wide,
  .metadata-fact-wide {
    grid-column: auto;
  }

  .page-setup-body {
    grid-template-columns: 1fr;
    gap: 18px;
  }

  .page-preview {
    min-height: 160px;
    padding: 16px;
  }

  .page-preview-sheet {
    width: 76px;
  }

  .page-preview-sheet[data-orientation="landscape"] {
    width: 130px;
  }

  /* One column on a phone keeps each group wide enough for its controls. */
  .page-setup-fields {
    columns: 1;
  }

  .page-setup-fields > .dialog-group {
    margin-bottom: 18px;
  }
}

/* =========================================================================
   The phone chrome (docs/148, ADR-044)
   -------------------------------------------------------------------------
   `body.phone-mode` is set by `phone_chrome.mjs` at or below 620px — the SAME
   rung the block above uses, deliberately, because a fourth breakpoint that
   only the phone knows about is the mistake that block already argues against.
   A class rather than only a media query, for HF-088's reason: the SHAPE
   changes in script too (the ribbon stops being the navigation axis), and a
   media query cannot tell main.js which shape it picked.

   THE RULE THIS EXISTS TO KEEP: no horizontal scroll, at any phone width, on
   any chrome surface. Measured on `main` at 390x844 before this block, with a
   document open:

     .ribbon-tabs   scrollWidth 550  clientWidth 109   ← 5 of 8 tabs behind a
                                                         sideways gesture
     #viewport      scrollWidth 794  clientWidth 326   ← the document itself
     .app-menu-bar  clips under a mask at 360px (responsive-shell.spec.mjs)

   The first two were two different problems. The tab strip is chrome and chrome
   must fit, which is solved here; the third is solved by letting the bar wrap.
   `#viewport` is the DOCUMENT, and it WAS the named exception in `docs/148` §8:
   a 6.5in text column cannot be both readable and 390px wide, which is why every
   reference ships a reflow view (Google's Pageless, ONLYOFFICE's
   `api.ChangeReaderMode()` / `CReaderTouchManager`) and why `view_zoom.mjs`'s
   `FIT_ON_OPEN_FLOOR` deliberately refuses to shrink a page to the 31% that
   would make it fit.

   REFLOW LANDED 2026-10-01 (ADR-046, `docs/151`) AND THE EXCEPTION IS GONE.
   Reflow defaults on below this same rung, lays the body out at the window's
   width and cuts it into tiles, so `#viewport` now measures 384 into 390. The
   tile styling is `#viewport.is-reflow` above; `FIT_ON_OPEN_FLOOR` is unchanged
   and still refuses the 31%, because the fix was to stop needing it rather than
   to lower it.

   WHAT MOVES, and why each:
     header   ONE row, brand mark dropped; the menu bar is a sheet behind a
              menus button (`docs/148` §5.3b). It first WRAPPED instead of
              scrolling under a mask, which cost 93px at 390 and 120px at 320
     ribbon   gone — `body.compact-mode` already hides it, and `setChromeMode`
              forces compact at this rung
     rail     turns its axis: a horizontal strip above the document instead of
              a column beside it, so it costs height rather than width. Kept
              rather than hidden because its Pages tile is `#pagesPanel`'s only
              surface (see `phone_chrome.mjs`'s `phoneRegions`)
     toolbar  the compact bar docks to the BOTTOM, above the status bar and
              above the soft keyboard. All three references put the phone's
              command surface at the bottom: Google Docs' keyboard-attached
              row, Word for mobile's bottom ribbon sheet, ONLYOFFICE's Edit
              sheet (`apps/documenteditor/mobile/src/view/edit/Edit.jsx`)
     panels   bottom sheets, the shape `.review-sheet` already established here
     dialogs  bottom sheets; a card can then never be wider than the window
     menus    a dropdown hung off a bottom bar opens upward, as a sheet

   Touch targets are 24px minimum (WCAG 2.5.8 Target Size (Minimum), Level AA),
   following this file's existing habit of drawing small and hitting big rather
   than making everything visually bigger. `phone_chrome.mjs` exports the same
   number as `MIN_TOUCH_TARGET_PX` and `phone_chrome.test.mjs` fails if the two
   drift.
   ========================================================================= */

body.phone-mode {
  /* One number for everything pinned to the bottom edge. The keyboard inset is
     published by `phone_chrome.mjs` from `visualViewport`; on a browser that
     honours `interactive-widget=resizes-content` (the meta tag in editor.html)
     the layout viewport has already shrunk and this reads 0, which is correct. */
  --phone-bottom-inset: calc(var(--phone-keyboard-inset, 0px) + env(safe-area-inset-bottom, 0px));
  /* The docked bar's height, stated rather than borrowed. Every bottom reserve
     on this rung — the sheets' bottom edge, the document's padding, the toast —
     used to read `--h-compact-bar`, which is `clamp(38px, 3.4vh, 46px)` and
     resolves to 38px on any phone, while the bar actually paints 41px (34px
     controls, 3px padding each side, a 1px rule) and, under a finger, 51px
     (the coarse block's 44px controls). So every sheet's bottom edge lay 3px —
     and on a touch phone 13px — across the top of the bar's own buttons
     (`narrow-review-column.spec.mjs` recorded the 3px; `phone-09f` shows the
     13). The global token is left alone (`SKILL.md` §11); the rung states its
     own number and `body.phone-mode .compact-toolbar` below is held to it, so
     the two cannot disagree again (`109` HF-278). */
  --phone-bar-height: 41px;
  --phone-bottom-chrome: calc(var(--h-footer) + var(--phone-bar-height) + var(--phone-bottom-inset));
}

@media (pointer: coarse) {
  body.phone-mode {
    --phone-bar-height: 51px;
  }
}

/* ---- Header: ONE row, no brand, the menu bar behind a button ------------ */

/* The brand mark is the first thing a phone gives up — it names the product to
   someone already inside it, and it is the only header cell that can be spent
   without taking a capability with it. Its grid column goes with it. */
body.phone-mode .brand {
  display: none;
}

/* The Compact/Ribbon toggle goes too, and it is the one removal that is about
   honesty rather than width: at this rung `setChromeMode` forces compact
   whatever the radio says, so leaving the pair on screen would be a live
   control that cannot do the thing it names — "never a dead control"
   (SKILL.md §10). The preference itself is untouched and the toggle comes
   straight back when the window does. */
body.phone-mode .chrome-mode {
  display: none;
}

/* ONE ROW: the document's name, its state, and three buttons — the menus, its
   properties, settings. Google Docs' phone header is exactly this shape (the
   document and a few icons, everything else behind one control), and so are
   Word mobile's and ONLYOFFICE mobile's (`docs/148` §2-§4).

   It was three rows. The menu bar wrapped File Edit View Insert Format Table /
   References Review onto two lines under the title, measured at 92px of an
   844px window (`phone-09a`), and with the docked bar and the status bar the
   chrome took ~40% of the screen before a soft keyboard took its own half.
   `docs/148` §5.3 kept the bar on screen because "an axis may not be
   abbreviated", and that is still the rule: nothing here abbreviates it. All
   eight names are kept, in order, each opening its own menu — they are drawn
   in a sheet the menus button opens instead of across the header
   (`menu_sheet.mjs`, §5.3b). */
body.phone-mode .bar {
  grid-template-columns: minmax(0, 1fr) auto auto auto;
  grid-template-rows: auto;
  grid-template-areas: "title docstate menus apptools";
  align-items: center;
  height: auto;
  /* Paint under a notch, then pay for it (`viewport-fit=cover`). */
  padding: calc(4px + env(safe-area-inset-top, 0px)) calc(8px + env(safe-area-inset-right, 0px)) 4px
    calc(8px + env(safe-area-inset-left, 0px));
}

/* The door. Not painted at any other width, where the bar is the row. */
.app-menus-button {
  display: none;
}

body.phone-mode .app-menus-button {
  display: inline-flex;
  grid-area: menus;
}

body.phone-mode .app-menus-button[aria-expanded="true"] {
  background: var(--accent-soft);
  color: var(--accent-text);
}

/* The bar itself is not painted in the row… */
body.phone-mode .app-menu-bar {
  display: none;
}

/* …and when the door is open it is a bottom sheet of eight rows, where every
   other phone sheet is: the menu each name opens is a bottom sheet in the same
   place (see `.app-menu-popover` below), so choosing one REPLACES the list
   rather than stacking a second card beside it. The list is not scrolled
   sideways and not masked: it is a column, and it is as tall as it needs up to
   the room between the header and the docked bar. */
body.phone-mode .app-menu-bar.is-open {
  position: fixed;
  right: 0;
  bottom: calc(var(--h-footer) + var(--phone-bar-height) + var(--phone-bottom-inset));
  left: 0;
  z-index: var(--z-menu);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  height: auto;
  max-height: calc(100dvh - var(--phone-header-height, var(--h-header)) - var(--phone-bottom-chrome) - 8px);
  margin: 0;
  padding: 6px;
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--line-strong);
  border-bottom: 0;
  border-radius: var(--chrome-card-radius) var(--chrome-card-radius) 0 0;
  background: var(--surface);
  box-shadow: var(--shadow-popover);
  mask-image: none;
  -webkit-mask-image: none;
}

/* A fixed box inside the header is still inside the header's stacking context
   (`.bar` is a flex item of `body` with a z-index), so without this the sheet
   would sit at `--z-bar` under every drawer and object toolbar on the page.
   Raised only while it is open. The menu a name opens is `--z-menu` too and
   comes later in the document, so it still paints above the list. */
body.phone-mode:has(#appMenuBar.is-open) .bar {
  z-index: var(--z-menu);
}

/* While a menu is open the list steps out of sight; Escape brings it back with
   focus on the name that opened it — a drill, the shape a submenu already has
   at this rung. `visibility`, not `display`, so the names keep their boxes and
   that focus has somewhere to land the moment the menu closes. */
body.phone-mode:has(#appMenuPopover:not([hidden])) .app-menu-bar.is-open {
  visibility: hidden;
}

/* Rows a finger can hit. The coarse-pointer block below raises floating menu
   rows to 44px and these are floating rows now, so they take the same floor;
   with a mouse they keep the dense 36px the rest of this rung's menus use. */
body.phone-mode .app-menu-bar.is-open .app-menu-button {
  height: auto;
  min-height: 36px;
  padding: 6px 12px;
  border-radius: var(--radius-sm);
  line-height: normal;
  text-align: start;
}

@media (pointer: coarse) {
  body.phone-mode .app-menu-bar.is-open .app-menu-button {
    min-height: 44px;
  }
}

/* ---- The work area ----------------------------------------------------- */

body.phone-mode .workarea {
  flex-direction: column;
  min-width: 0;
}

/* ---- The rail and the ruler are not painted (docs/148 §5.3a) ------------ */

/* The first version of this rung turned the rail's AXIS — a 40px column became
   a horizontal strip — rather than withholding it, and kept the ruler, and said
   why: `#railPages` was `#pagesPanel`'s only surface and `ruler.mjs` held the
   only calls to `setTabStop`, so hiding either would have made a capability
   unreachable at 390px that is reachable at 1280px. Google Docs, Word mobile
   and ONLYOFFICE mobile all ship neither, and §5.3 was arguing against all
   three references on that one point.

   `view.pages` and `layout.tabStops` now exist, so the argument is paid off and
   the phone spends the height on the document: ~44px of strip and ~24px of
   ruler. Against 844px that is 8%; against what is LEFT once a soft keyboard
   has taken ~300px it is ~18%, which is the number a reader feels.

   `display: none` on the NAV only. `#outlinePanel`, `#pagesPanel`,
   `#reviewSidebar` and the version panel are siblings of `.rail`, not children,
   and they stay: this withholds the rail's TILES, not its destinations, which
   open as bottom sheets from the View and Review menus. That distinction is
   the whole reason this is safe, and it is also why it is NOT the same as
   `body.chrome-no-rail` — a host withholding the `rail` REGION withholds the
   panels with it, and `main.js` correspondingly withholds `view.outline` and
   `view.pages` in that case. `phone_chrome.test.mjs` fails if either command id
   disappears from the taxonomy while these rules stand. */
body.phone-mode .rail,
body.phone-mode .ruler {
  display: none;
}

/* The zoom READOUT, for a third variant of the same argument. `#zoom` is a text
   field that lives in the status bar and is adopted into the compact bar; at
   320px the bar has no room for it and the status bar reported scrollWidth 344
   against clientWidth 320 with it left behind — the one kind of overflow this
   rung forbids. At 390px it fitted, and cost the bar its inline B/I/U, which is
   a worse trade: three one-tap toggles for a percentage nobody types on a
   phone. Neither reference shows one (Docs and ONLYOFFICE mobile both zoom by
   pinch, and pinch is deliberately not suppressed here — §5.5), and zoom stays
   reachable from the two steppers beside it, from View ▸ Zoom and from the
   palette. Hidden rather than removed, so `one-axis-navigation.spec.mjs`'s
   `view.zoom.` value-family exemption still finds its one element. */
body.phone-mode #zoom {
  display: none;
}

/* ---- The command surface docks to the bottom --------------------------- */

/* Google Docs, Word for mobile and ONLYOFFICE mobile all put the phone's
   formatting controls at the bottom of the screen, because that is where the
   thumb is and because the keyboard owns the bottom half anyway. This bar
   already folds what does not fit into its "⋯" menu rather than scrolling
   sideways (`compact_toolbar.mjs`), so docking it needs no second mechanism —
   which is the whole reason the phone chrome is this bar moved rather than a
   new bar written. */
body.phone-mode .compact-toolbar {
  position: fixed;
  right: 0;
  bottom: calc(var(--h-footer) + var(--phone-bottom-inset));
  left: 0;
  z-index: var(--z-bar);
  margin: 0;
  padding-inline: calc(8px + env(safe-area-inset-left, 0px)) calc(8px + env(safe-area-inset-right, 0px));
  border-top: 1px solid var(--line);
  border-radius: 0;
  /* Held to the number every bottom reserve on this rung reads (see
     `--phone-bar-height` at the top of this block). */
  height: var(--phone-bar-height);
  min-height: 0;
}

/* The bar's own buttons. The `(pointer: coarse)` block further down raises
   FLOATING controls to 44/48 (Apple HIG / Material); this is the dense-band
   floor — the same 24 the menu bar carries and the same number
   `phone_chrome.mjs` exports as `MIN_TOUCH_TARGET_PX` (WCAG 2.5.8 Target Size
   (Minimum), Level AA). Stated explicitly because this bar is now the phone's
   primary command surface and the rail that used to carry this rung's only
   other explicit floor is no longer painted. */
body.phone-mode .compact-toolbar .ctool {
  min-width: 24px;
  min-height: 24px;
}

/* Both bottom bars leave the flow, so the document has to be told not to end
   underneath them. */
body.phone-mode .viewport {
  padding-bottom: calc(var(--phone-bottom-chrome) + 24px);
}

body.phone-mode .footer {
  position: fixed;
  right: 0;
  bottom: var(--phone-keyboard-inset, 0px);
  left: 0;
  z-index: var(--z-bar);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  border-top: 1px solid var(--line);
}

/* The toast is pinned to the viewport's bottom-start corner, one footer's
   height up — which is precisely where the command bar now is. `109` HF-233 is
   the same defect one surface earlier: the toast landing on a narrow-screen
   drawer's footer action, arbitrated by reserving `--h-toast-band` rather than
   by an argument about z-index. The bar is a second thing sharing the bottom
   edge, so it gets the same arbitration.

   FOUND BY LOOKING, not by a spec: the toast is `pointer-events: none`, so no
   assertion about clicking the bar would ever have failed, and "Rendering 1
   page at 100%…" sat across the style picker in every screenshot taken of this
   rung. The guard added with the fix measures the overlap instead. */
body.phone-mode .toast {
  bottom: calc(var(--phone-bottom-chrome) + var(--space-3));
}

/* ...and the same arbitration one layer further out, for a sheet.
   `docs/148` §9 item 8: the toast still painted over an OPEN bottom sheet,
   because `--z-toast` is above `--z-modal` deliberately and a sheet occupies
   the same bottom-start corner. It was recorded rather than fixed, with a
   reason: "reserving a band above whatever sheet is open needs the sheet's
   height, which is content-dependent".

   That reason is sound and it is an argument against ONE answer, not against
   all of them. A sheet's height cannot be known in CSS; where the sheet is NOT
   is entirely knowable — it is the top of the screen. So while a sheet is open
   the toast moves to the top, under the header, which is a placement Material
   already specifies for a snackbar and which cannot overlap whatever the sheet
   turns out to be. The rule needs no measurement, no JS and no z-index
   argument, which is the same way `109` HF-233 was settled: arbitrate by
   position, not by stacking order.

   `:has()` on `body` rather than a class toggled in script, because every one
   of these surfaces is opened by a different owner — `popover_manager.mjs` for
   the menus, `modal.mjs` for the dialogs, `main.js` for the panels — and a
   class would be a fourth thing each of them had to remember. The selector is
   true by construction instead.

   The dialog overlay is in the list because at this rung a dialog IS a bottom
   sheet (see `.dialog-overlay` above), so it has exactly the same collision. */
/* The list is exactly the surfaces capped at `max-height: 60vh`, and that bound
   is what makes the rule safe rather than a preference: 60vh of 844 leaves the
   sheet's top edge at ~266px, and a toast under a two-row header sits at
   ~105-141. The other two candidates are deliberately NOT here, and both were
   tried and measured:

     `.side-panel`  — at this rung `.version-panel` is a top-anchored DRAWER,
                      not a bottom sheet: measured 123→587 at 390px. There is no
                      band above it, and moving the toast up put it across the
                      drawer's head. That collision is already solved the other
                      way — `109` HF-233 reserves `--h-toast-band` at the
                      drawer's foot — and `editor-narrow-chrome.spec.mjs`
                      measures it.
   A modal is in the list through `body.modal-open`, which `modal.mjs` already
   sets, rather than through a `:has()` of its own. It was excluded on a first
   pass on the theory that a dialog card capped at `max-height: 86vh` leaves
   only ~118px above it — but that is the CAP and not the height, and the
   screenshot settled it: the Tab stops sheet stands about 60% tall at 390px,
   and the toast lay straight across its Set and Clear all buttons. Even at the
   cap the top placement is the better of the two collisions, because it lands
   on the card's HEAD rather than on its pinned actions, and `109` HF-233's rule
   is that the pinned action stays readable. */
body.phone-mode.modal-open .toast,
body.phone-mode:has(.app-menu-popover:not([hidden])) .toast,
body.phone-mode:has(.compact-command-menu:not([hidden])) .toast,
body.phone-mode:has(.compact-align-menu:not([hidden])) .toast,
body.phone-mode:has(.compact-overflow-menu:not([hidden])) .toast,
body.phone-mode:has(.grid-picker:not([hidden])) .toast,
body.phone-mode:has(#appMenuBar.is-open) .toast {
  /* `--phone-header-height` and NOT `--h-header`: the token says 63px and this
     rung's header is two rows at 390px and three at 320px, because the menu bar
     wraps rather than scrolling (§5.3) and how many rows that takes depends on
     the locale. The first version of this rule used the token and the toast
     landed across "Format Table References" — the same defect one row up, found
     the same way, by looking. `phone_chrome.mjs` publishes the measured height
     from a `ResizeObserver`; the fallback keeps the rule sane in a build where
     nothing published it. */
  top: calc(var(--phone-header-height, var(--h-header)) + var(--space-3) + env(safe-area-inset-top, 0px));
  bottom: auto;
}

/* ---- Panels, dialogs and menus are bottom sheets ----------------------- */

/* The shape `.review-sheet` already introduced at this rung (HF-088), applied
   to the panels that were still trying to be a column: at 390px a 340px drawer
   leaves the document 50px, which is a panel with a document-shaped sliver
   beside it rather than a document with a panel over it. */
body.phone-mode .side-panel {
  position: fixed;
  right: 0;
  bottom: calc(var(--h-footer) + var(--phone-bottom-inset));
  left: 0;
  z-index: var(--z-drawer);
  width: auto;
  max-width: none;
  max-height: 55vh;
  margin: 0;
  border-radius: var(--chrome-card-radius) var(--chrome-card-radius) 0 0;
  box-shadow: var(--shadow-popover);
  flex: none;
}

/* A dialog centred in a 320px window with 24px of overlay padding on each side
   is a 272px card holding controls sized for a laptop. As a bottom sheet it
   takes the width it needs and can never be the thing that overflows — which
   is what this rung is for. Material 3 calls this a modal bottom sheet and
   caps it at half the screen; ours goes higher because a document-properties
   form is taller than a chooser and its own body already scrolls. */
body.phone-mode .dialog-overlay {
  padding: 0;
  place-items: end stretch;
}

body.phone-mode .dialog-card {
  width: 100%;
  max-width: 100%;
  max-height: 86vh;
  padding-bottom: env(safe-area-inset-bottom, 0px);
  border-radius: var(--chrome-card-radius) var(--chrome-card-radius) 0 0;
}

/* `popoverPosition` clamps a menu's LEFT edge to the window and then lets a
   menu wider than the window run off the right — `Math.max(gutter, width -
   menu.width - gutter)` cannot do anything else. A ceiling on the menu is the
   half it cannot compute, and it belongs here rather than in the arithmetic,
   because it is a property of the window and not of the anchor. */
body.phone-mode .context-menu,
body.phone-mode .app-menu-popover,
body.phone-mode .ribbon-overflow-menu,
body.phone-mode .cmd-palette {
  max-width: calc(100vw - 16px);
}

/* A dropdown hung off a bar at the BOTTOM of the screen has nowhere below to
   open into, so it opens as a sheet instead. `!important` is not decoration:
   `popover_position.mjs` writes `left`/`top` as inline style, and an inline
   declaration outranks every selector that is not marked. The alternative is
   teaching that module about a breakpoint, which would put the rung in a third
   place. */
body.phone-mode .app-menu-popover,
body.phone-mode .compact-command-menu,
body.phone-mode .compact-align-menu,
body.phone-mode .compact-overflow-menu,
body.phone-mode .grid-picker {
  position: fixed !important;
  top: auto !important;
  right: 0 !important;
  bottom: calc(var(--h-footer) + var(--phone-bar-height) + var(--phone-bottom-inset)) !important;
  left: 0 !important;
  width: auto !important;
  max-width: none;
  max-height: 60vh;
  overflow-y: auto;
  /* A sheet that chains its overscroll drags the shell behind it (`style.css`'s
     root `overscroll-behavior: none` and `style_tokens.test.mjs`'s guard, which
     caught this rule the first time it was written without it). */
  overscroll-behavior: contain;
  border-radius: var(--chrome-card-radius) var(--chrome-card-radius) 0 0;
}

/* The table-size grid joins the sheets above, for the reason they are sheets
   (`109` HF-275).
   On a phone it is opened from the + sheet, whose row has vanished by the time
   the grid measures its anchor, so `popoverAnchor` fell back to the ribbon's
   hidden Insert-table button and `popoverPosition` put the grid at the window's
   top-left — measured at y 4, across the header and the menu bar
   (`phone-08-insert-table.png`). A sheet has no anchor to lose. And once it is
   a sheet the cells can spend the width: ten columns of at least the 24px
   floor, at most 40 so a 620px window does not get a grid taller than the
   60vh the sheet may take. 390px gives ~34px cells, 320px ~27px. */
body.phone-mode .grid-cells {
  grid-template-columns: repeat(10, minmax(24px, 40px));
  justify-content: center;
  gap: 3px;
}

body.phone-mode .grid-cells .gc {
  width: auto;
  height: auto;
  aspect-ratio: 1;
}

/* =========================================================================
   Coarse pointers — touch and pen (HF-060)
   -------------------------------------------------------------------------
   Keyed on the pointing MODALITY, not on window width, because the question
   is what is doing the pointing and not how wide the window is: a 13-inch
   touchscreen laptop is a wide window with a finger on it, and a 500px window
   driven by a mouse still wants mouse density. Every rung above this one is a
   `max-width` rung and none of them would have caught either case.

   Two things change here and nothing else.

   1. Fields go to 16px. Below 16px iOS Safari zooms the page on focus and
      leaves the document scrolled sideways with no way back — so on iPad and
      iPhone every single tap into a dialog field, the find box, the document
      title or a comment composer wrecked the view. 16px is the documented
      threshold that turns it off, not a taste call.
      The selectors are shaped by specificity, not by taste. Fields all over
      this file set their own size from class-scoped rules — `.find-panel
      input[type="text"]` and `.ctl > input[type="number"]` at (0,2,1),
      `.dialog-field > textarea` and the review composers at (0,1,1) — and a
      bare `input` / `textarea` selector loses to every one of them, silently.
      The chained `:not()`s do double duty: they exclude the inputs that render
      no text (colour, range, checkbox, radio — a size there only moves their
      swatch geometry) AND each adds a class's worth of weight, which is what
      lifts this rule over those without reaching for `!important`. Same reason
      `textarea` carries one.

   2. Targets a finger has to hit reach the 44px HIG floor. The growth is spent
      ONLY on surfaces that float over the document — menus, the command
      palette, context menus, the ribbon's overflow flyout, the comment column,
      the object toolbar, panel headers. Those are painted over the page, so
      44px rows cost the canvas nothing; the ribbon band, the tab strip and the
      footer keep their desktop heights for exactly that reason. It matters
      most where the rows are destructive: in the context menu "Delete row" is
      two rows from "Merge cells".

   Deliberately NOT here, and tracked rather than half-done: the header's own
   `.app-menu-button` row (21px inside a committed 58px band — a 44px floor
   there needs the header redesigned, not restyled), and the eight object
   resize grips, whose targets are already widened to ~25px by `.object-handle
   ::after` but cannot go to 44px without adjacent handles overlapping on a
   small image, which needs hit-test arbitration in main.js.
   ========================================================================= */
@media (pointer: coarse) {
  input:not([type="color"]):not([type="range"]):not([type="checkbox"]):not([type="radio"]),
  select:not([multiple]),
  textarea:not([hidden]) {
    font-size: 16px;
  }

  /* Rows in floating menus: the app menus, context menus and submenus, the
     command palette, and dialog choice lists. */
  .app-menu-item,
  .menu-item,
  .cmd-item,
  .field-choice {
    min-height: 44px;
  }

  /* Icon-only controls on floating surfaces. */
  .panel-close,
  .review-nav-button,
  .ribbon-overflow-menu .fmt {
    width: 44px;
    height: 44px;
    min-width: 44px;
  }

  /* Compact pills inside the floating comment column and the object toolbar. */
  .review-filter,
  .review-card-action,
  .object-bar-btn {
    min-height: 44px;
  }

  /* The margin comment affordance reaches the finger floor by GROWING ITS
     TARGET, not its box — the same trick as the image resize grip (HF-098), and
     for a stricter reason here: its 32px box is what `commentAffordanceSpot`
     measures the page's right margin against, so a box that changed with the
     pointer would make the "does this fit beside the page" answer depend on the
     input device. 32px visual, 44px hit area. */
  .review-margin-add::before {
    content: "";
    position: absolute;
    inset: -6px;
  }
}

/* =========================================================================
   Formatting toolbar UX v2 (Q1–Q5): font menu, color/highlight pickers,
   grow/shrink, change case, editable zoom, paste-options chip.
   Dense, industry-standard controls; light/dark + focus rings throughout.
   ========================================================================= */

.visually-offscreen {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  border: 0;
}

/* ---- Font family trigger (replaces the native <select>) -------------------- */
.font-family-trigger {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  /* A font name that reads "Liberation S…" tells the user nothing about which
   * face they are in. 122px could not hold the metric-compatible names this
   * engine substitutes by default. */
  /* Deliberately narrow. The Home band has a hard 1280px no-scrollbar budget
     (docs/64) and only about 55px of slack in it; widening this control enough
     to hold a long family name eats most of that and pushes the Styles gallery
     into the overflow menu, which is a worse trade than an ellipsis. Word makes
     the same call at a similar width. The complete name is never lost — the
     trigger's `title` always carries it, asserted in ribbon-legibility.spec.mjs. */
  width: 122px;
  height: var(--h-control);
  padding: 0 4px 0 8px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font-size: var(--fs-body);
  cursor: pointer;
  transition: border-color 0.11s, background 0.11s;
}

.font-family-trigger:hover:not(:disabled) {
  border-color: var(--accent-line);
}

.font-family-trigger:disabled {
  opacity: 0.5;
  cursor: default;
}

.font-family-trigger:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

.font-family-trigger[aria-expanded="true"] {
  border-color: var(--accent-text);
}

.font-family-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
}

.font-family-trigger.is-placeholder .font-family-label {
  color: var(--faint);
}

.font-family-caret {
  flex: 0 0 auto;
  font-size: 18px;
  color: var(--muted);
}

.ctl.is-mixed .font-family-trigger {
  border-style: dashed;
}

/* ---- Grow / shrink font + change-case glyphs ------------------------------- */
.grow-shrink .grow-glyph {
  position: relative;
  display: inline-flex;
  align-items: baseline;
  font-weight: 600;
  line-height: 1;
  font-size: 12px;
}

.grow-shrink .grow-glyph-big {
  font-size: 16px;
}

.grow-shrink .grow-sign {
  font-size: 9px;
  font-weight: 700;
  margin-left: 1px;
}

.case-glyph {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.02em;
}

/* ---- Text-color + highlight split pickers ---------------------------------- */
.color-control {
  display: inline-flex;
  align-items: stretch;
  gap: 0;
  padding: 0;
  height: var(--h-control);
  border-radius: var(--radius-sm);
}

.color-apply {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  width: 26px;
  height: var(--h-control);
  padding: 0;
  border: 1px solid transparent;
  border-right: 0;
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: background 0.11s;
}

.color-apply-glyph {
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
}

.color-apply .ms.color-apply-glyph {
  font-size: var(--icon-control);
  font-weight: 400;
}

.color-apply-bar {
  display: block;
  width: 16px;
  height: 4px;
  border-radius: 1px;
  background: var(--ink);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.12) inset;
}

.color-apply-bar.is-none {
  background:
    linear-gradient(45deg, transparent 44%, var(--danger) 44% 56%, transparent 56%),
    var(--surface-alt);
}

/* Wide enough to HOLD its glyph. It was 16px with a 1px right border, so the
   18px `arrow_drop_down` was squeezed to 15px and cut off on its right: a flex
   item with `overflow: hidden` (which every `.ms` is) has an automatic minimum
   width of zero, so it shrinks silently instead of overflowing visibly. The
   box is stated FROM the glyph token, so the two cannot drift apart again. */
.color-caret {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--icon-control) + 1px);
  height: var(--h-control);
  padding: 0;
  border: 1px solid transparent;
  border-left: 0;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.color-caret .ms {
  font-size: 18px;
}

.color-apply:hover:not(:disabled),
.color-caret:hover:not(:disabled) {
  background: var(--bg-2);
}

.color-apply:disabled,
.color-caret:disabled {
  opacity: 0.35;
  cursor: default;
}

.color-apply:focus-visible,
.color-caret:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
  border-radius: var(--radius-sm);
}

.color-caret[aria-expanded="true"] {
  background: var(--accent-soft);
  color: var(--accent-text);
}

/* Split list buttons: the main toggle keeps its plain `.fmt` look while a
   slim caret opens the marker-format gallery, mirroring Word/Docs. */
.list-split {
  display: inline-flex;
  align-items: stretch;
  gap: 0;
}

.list-split .list-split-main {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

/* The same rule as `.color-caret`: the box holds the glyph. 14px less two
   borders left 12px of an 18px caret on screen. */
.list-caret {
  width: calc(var(--icon-control) + 2px);
  min-width: calc(var(--icon-control) + 2px);
  padding: 0;
  color: var(--muted);
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

.list-caret .ms {
  font-size: 18px;
}

.list-caret[aria-expanded="true"] {
  background: var(--accent-soft);
  color: var(--accent-text);
}

.underline-split.is-mixed .list-caret {
  color: var(--accent-text);
}

#underline .underline-glyph {
  text-decoration-color: var(--underline-color, currentColor);
}

#underline[data-underline-style="double"] .underline-glyph {
  text-decoration-style: double;
}

#underline[data-underline-style="thick"] .underline-glyph {
  text-decoration-thickness: 3px;
}

#underline[data-underline-style="dotted"] .underline-glyph {
  text-decoration-style: dotted;
}

#underline[data-underline-style="dashed"] .underline-glyph,
#underline[data-underline-style="dotDash"] .underline-glyph {
  text-decoration-style: dashed;
}

#underline[data-underline-style="wavy"] .underline-glyph {
  text-decoration-style: wavy;
}

/* Marker-format gallery: a compact grid of preview cells. */
.list-gallery-menu {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
  padding: 6px;
}

.list-gallery-menu-number {
  grid-template-columns: repeat(5, 1fr);
}

.list-gallery-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--border, rgba(0, 0, 0, 0.12));
  border-radius: var(--radius-sm);
  background: var(--surface, transparent);
  color: var(--ink);
  cursor: pointer;
  transition: background 0.11s, border-color 0.11s;
}

.list-gallery-glyph {
  font-size: 20px;
  line-height: 1;
}

.list-gallery-sample {
  font-size: 10px;
  line-height: 1.15;
  text-align: center;
  color: var(--ink);
}

.list-gallery-cell:hover,
.list-gallery-cell:focus-visible {
  background: var(--bg-2);
  border-color: var(--accent-text);
  outline: none;
}

.list-gallery-cell[aria-checked="true"] {
  border-color: var(--accent-text);
  background: var(--accent-soft);
  color: var(--accent-text);
}

.list-gallery-cell[aria-checked="true"] .list-gallery-sample {
  color: var(--accent-text);
}

/* Keep the last Font control clear of the Paragraph divider (mirrors the old
   highlight control's 10px right margin the ribbon-home suite guards). */
[data-group="font"] .color-control-highlight {
  margin-right: 10px;
}

.ctl.color-control.is-mixed .color-apply-bar {
  background: linear-gradient(135deg, var(--muted) 0 45%, transparent 45% 55%, var(--muted) 55%);
}

/* ---- Insert ▸ Shapes gallery ----------------------------------------------
   Word's gallery: icon cells in a grid under category headings, each cell an
   SVG outline of the preset it inserts. Seven of them used to be drawn with
   `clip-path`; twenty-two are drawn from the one catalogue table instead, so a
   preset cannot be modeled, painted and unofferable (`shape_catalogue.mjs`). */
#shapeGalleryMenu {
  max-width: 260px;
  padding-bottom: 6px;
}

.shape-group-heading {
  padding-top: 6px;
}

.shape-grid {
  display: grid;
  grid-template-columns: repeat(var(--shape-gallery-columns, 6), minmax(0, 1fr));
  gap: 2px;
  padding: 2px 4px 4px;
}

.shape-cell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 24px is the WCAG 2.5.8 target minimum; the cell is 30 so a thumb has room
     without the gallery needing a second, larger, touch-only layout. */
  min-width: 30px;
  min-height: 30px;
  padding: 4px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  cursor: pointer;
}

.shape-cell:hover,
.shape-cell:focus-visible {
  border-color: var(--accent-text);
  background: var(--accent-soft);
  outline: none;
}

.shape-cell-preview {
  width: 20px;
  height: 20px;
  fill: var(--accent-soft);
  stroke: var(--accent-text);
  stroke-width: 1.4;
  stroke-linejoin: round;
}

.shape-cell-preview line {
  stroke-width: 2;
}

.shape-cell:hover .shape-cell-preview,
.shape-cell:focus-visible .shape-cell-preview {
  fill: color-mix(in srgb, var(--accent) 28%, transparent);
}

/* The rubber band drawn while a shape is being dragged out. It shows the PRESET
   being drawn, not a generic rectangle: the rectangle is what the user is
   sizing, the outline is what they picked, and showing only the first is how a
   direct-manipulation gesture turns back into a guess. */
.shape-draw-preview {
  position: absolute;
  pointer-events: none;
  outline: 1px dashed var(--accent);
  outline-offset: 0;
}

.shape-draw-preview svg {
  width: 100%;
  height: 100%;
  fill: color-mix(in srgb, var(--accent) 18%, transparent);
  stroke: var(--accent);
  stroke-width: 2;
  vector-effect: non-scaling-stroke;
}

/* The armed pointer. A mode with no visible cursor is a mode the user cannot
   see the edge of, which is why Escape also announces leaving it. */
body[data-shape-draw] .page-wrap,
body[data-shape-draw] .page-wrap * {
  cursor: crosshair;
}

/* ---- Object ▸ Position gallery (Word's nine cells) ------------------------ */
.object-position-menu {
  padding: 6px;
}

.object-position-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
}

.object-position-cell {
  display: block;
  padding: 3px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  cursor: pointer;
}

.object-position-cell:hover:not(:disabled),
.object-position-cell:focus-visible {
  border-color: var(--accent-text);
  background: var(--accent-soft);
  outline: none;
}

.object-position-cell:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

.object-position-sheet {
  position: relative;
  display: block;
  /* 34x44 keeps the whole cell above the 24px touch minimum with its padding. */
  width: 34px;
  height: 44px;
  border: 1px solid var(--line);
  border-radius: 2px;
  background: var(--surface);
}

.object-position-mark {
  position: absolute;
  width: 12px;
  height: 10px;
  background: var(--accent);
  border-radius: 1px;
}

.object-position-mark.pos-top-left { top: 3px; left: 3px; }
.object-position-mark.pos-top-center { top: 3px; left: 50%; transform: translateX(-50%); }
.object-position-mark.pos-top-right { top: 3px; right: 3px; }
.object-position-mark.pos-center-left { top: 50%; left: 3px; transform: translateY(-50%); }
.object-position-mark.pos-center-center { top: 50%; left: 50%; transform: translate(-50%, -50%); }
.object-position-mark.pos-center-right { top: 50%; right: 3px; transform: translateY(-50%); }
.object-position-mark.pos-bottom-left { bottom: 3px; left: 3px; }
.object-position-mark.pos-bottom-center { bottom: 3px; left: 50%; transform: translateX(-50%); }
.object-position-mark.pos-bottom-right { bottom: 3px; right: 3px; }

/* The check on an Arrange/Rotate row that names the current state. */
.menu-item-check {
  justify-self: end;
  color: var(--accent-strong);
}

/* ---- Shape Fill / Shape Outline (object context bar) ----------------------- */
/* The bar's two color buttons carry the shape's own color as an underline bar,
 * so the control reads what the shape HAS before it is opened. */
.object-bar-color {
  padding-right: 8px;
}

.object-bar-color .color-apply-bar {
  width: 14px;
  margin-left: 2px;
}

/* Word's Shape Outline > Weight list: each row previews its own thickness. */
.shape-weight-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.shape-weight-rule {
  display: block;
  width: 34px;
  height: var(--w, 1px);
  background: var(--ink);
  border-radius: 1px;
  flex: none;
}

.shape-weight.is-active {
  background: var(--surface-alt);
  font-weight: 600;
}

/* ---- Color picker menu ----------------------------------------------------- */
.color-menu {
  min-width: 0;
  width: max-content;
  padding: 8px;
}

.underline-menu {
  width: 226px;
  max-height: min(520px, calc(100vh - 24px));
  overflow-y: auto;
  overscroll-behavior: contain;
}

.underline-style-option {
  justify-content: space-between;
}

.underline-style-option[aria-checked="true"] {
  color: var(--accent-text);
  background: var(--accent-soft);
  font-weight: 600;
}

.underline-menu .color-row-action.is-active {
  color: var(--accent-text);
  background: var(--accent-soft);
}

.underline-style-preview {
  display: inline-block;
  width: 74px;
  color: var(--ink);
  text-align: center;
  text-decoration-line: underline;
  text-underline-offset: 3px;
}

.underline-style-none {
  text-decoration: none;
}

.underline-style-double {
  text-decoration-style: double;
}

.underline-style-thick {
  text-decoration-thickness: 3px;
}

.underline-style-dotted {
  text-decoration-style: dotted;
}

.underline-style-dashed,
.underline-style-dotDash {
  text-decoration-style: dashed;
}

.underline-style-wavy {
  text-decoration-style: wavy;
}

.underline-style-words {
  text-decoration-style: solid;
  word-spacing: 5px;
}

.color-row-action {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  font-size: var(--fs-body);
  text-align: left;
  cursor: pointer;
}

.color-row-action:hover,
.color-row-action:focus-visible {
  background: var(--accent-soft);
  color: var(--accent-text);
  outline: none;
}

.color-chip {
  width: 15px;
  height: 15px;
  border-radius: 3px;
  background: var(--sw, var(--ink));
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2) inset;
}

.color-chip-none {
  background:
    linear-gradient(45deg, transparent 44%, var(--danger) 44% 56%, transparent 56%),
    var(--surface);
}

.color-more .ms {
  font-size: 16px;
  color: var(--muted);
}

.swatch-grid {
  display: grid;
  grid-template-columns: repeat(10, 18px);
  gap: 4px;
  padding: 4px 2px 6px;
}

.color-menu[aria-label="Highlight color"] .swatch-grid {
  grid-template-columns: repeat(8, 18px);
}

.swatch-cell {
  width: 18px;
  height: 18px;
  padding: 0;
  border: 1px solid rgba(0, 0, 0, 0.14);
  border-radius: 3px;
  /* --sw is written inline per swatch by the colour-menu builder. The fallback
     is the never-happens case made visible: without one the cell would render
     transparent and read as a working control that shows nothing. */
  background: var(--sw, var(--ink));
  cursor: pointer;
  transition: transform 0.08s;
}

.swatch-cell.is-light {
  border-color: var(--line-strong);
}

.swatch-cell:hover {
  transform: scale(1.12);
}

.swatch-cell:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

.swatch-cell.is-active {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 3px var(--accent-text);
}

/* ---- Font menu ------------------------------------------------------------- */
.font-menu {
  width: 248px;
  padding: 6px;
}

.font-menu-search {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  margin-bottom: 4px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
}

.font-menu-search .ms {
  font-size: 16px;
  color: var(--muted);
}

.font-menu-search input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-size: var(--fs-body);
  outline: none;
}

.font-menu-list {
  max-height: 320px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.font-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink);
  font-size: 15px;
  text-align: left;
  cursor: pointer;
}

.font-menu-check {
  flex: 0 0 16px;
  font-size: 16px;
  color: var(--accent-text);
  opacity: 0;
}

.font-menu-item.is-current .font-menu-check {
  opacity: 1;
}

.font-menu-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.font-menu-item:hover,
.font-menu-item.is-active,
.font-menu-item:focus-visible {
  background: var(--accent-soft);
  color: var(--accent-text);
  outline: none;
}

.font-menu-empty {
  padding: 14px 8px;
  color: var(--faint);
  font-size: var(--fs-small);
  text-align: center;
}

/* ---- Editable zoom control ------------------------------------------------- */
.zoom-value-control {
  display: inline-flex;
  align-items: center;
  border-radius: 5px;
}

.zoom-input {
  width: 62px;
  height: 22px;
  border: 1px solid transparent;
  border-radius: 5px 0 0 5px;
  background: transparent;
  color: var(--ink);
  font-size: var(--fs-small);
  font-variant-numeric: tabular-nums;
  text-align: center;
  cursor: text;
}

.zoom-input:hover {
  background: var(--bg-2);
}

.zoom-input:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: -1px;
  background: var(--surface);
}

.zoom-caret {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 22px;
  /* The zoom-presets caret drew as a DOT: the user-agent's 6px of padding a
     side left its 16px `arrow_drop_down` a 6px box, and the glyph — a flex
     item with `overflow: hidden` — shrank into it rather than overflowing. */
  padding: 0;
  border: none;
  border-radius: 0 5px 5px 0;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.zoom-caret .ms {
  font-size: var(--icon-compact);
}

.zoom-caret:hover,
.zoom-caret[aria-expanded="true"] {
  background: var(--bg-2);
  color: var(--ink);
}

.zoom-caret:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
}

.zoom-menu {
  min-width: 148px;
}

.zoom-menu .zoom-preset .menu-item-label,
.zoom-menu .zoom-fit .menu-item-label {
  font-variant-numeric: tabular-nums;
}

.zoom-menu .menu-check {
  width: 14px;
  height: 14px;
  flex: 0 0 14px;
  opacity: 0;
  background: currentcolor;
  -webkit-mask: var(--check-svg) center / contain no-repeat;
  mask: var(--check-svg) center / contain no-repeat;
  --check-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z'/%3E%3C/svg%3E");
}

.zoom-menu [aria-checked="true"] .menu-check {
  opacity: 1;
}

/* ---- Footer language picker ------------------------------------------------
   Zoom's neighbour, and deliberately zoom's shape: the same anchored menu, the
   same tick on the current choice. The note about machine translation is the
   one thing the list is not — it is prose about the choice rather than a choice
   — so it sits below the scroll region and stays put while nineteen endonyms
   move under it. */
.language-menu {
  display: flex;
  flex-direction: column;
  min-width: 208px;
  /* Shorter than a full-height menu on purpose: the control is at the BOTTOM of
     the window, so a menu tall enough to need the whole viewport is one whose
     first rows are painted over the document it belongs to. */
  max-height: min(56vh, calc(100vh - 16px));
  overflow: hidden;
}

.language-menu-list {
  overflow-y: auto;
  overscroll-behavior: contain;
}

.language-option {
  /* 24px of pointer target on a row of 13px text, which is the touch floor the
     footer's other controls already meet. */
  min-height: 24px;
}

.language-option[aria-checked="true"] {
  color: var(--accent-text);
}

.language-option[aria-checked="true"] .menu-check {
  opacity: 1;
}

.language-menu-note {
  flex: 0 0 auto;
  max-width: 34ch;
  margin: 5px 4px 1px;
  padding-top: 6px;
  border-top: 1px solid var(--line);
  white-space: normal;
}

/* ---- Paste-options chip ---------------------------------------------------- */
.paste-options {
  position: fixed;
  z-index: var(--z-paste-options);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 6px 5px 10px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-popover);
  box-shadow: var(--shadow-popover);
  color: var(--ink);
  font-size: var(--fs-small);
}

.paste-options[hidden] {
  display: none;
}

.paste-options-icon {
  font-size: 16px;
  color: var(--muted);
}

.paste-options-action {
  border: 0;
  border-radius: var(--radius-sm);
  padding: 4px 8px;
  background: var(--accent-soft);
  color: var(--accent-text);
  font-size: var(--fs-small);
  font-weight: 600;
  cursor: pointer;
}

.paste-options-action:hover,
.paste-options-action:focus-visible {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  outline: none;
}

.paste-options-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.paste-options-close .ms {
  font-size: 16px;
}

.paste-options-close:hover {
  background: var(--bg-2);
  color: var(--ink);
}

/* ---- Header / footer editing context ---------------------------------------- */
/* Every reference editor de-emphasises the body while running content is being
   edited, so it is obvious which layer the keystrokes are going to (docs/85
   §10.2). The page itself is a canvas, so the cue is drawn on the shell rather
   than composited into the raster: the pages dim, and the caret and selection
   the engine paints on the overlay stay at full strength above them. */
/* Deliberately NOT a dim over the canvas. The header is painted INTO the page
   raster, so dimming `.page` washed out the very content being edited and turned
   the whole sheet grey — the cue has to sit beside the band, not on top of it.
   Word and LibreOffice both mark the band with a boundary and a label instead. */
.running-band {
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
  border-bottom: 1px dashed var(--accent);
}

.running-band.is-footer {
  border-top: 1px dashed var(--accent);
  border-bottom: none;
}

.running-band-label {
  position: absolute;
  left: 4px;
  padding: 1px 6px;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 10px;
  line-height: 1.5;
  letter-spacing: 0.02em;
}

.running-band:not(.is-footer) .running-band-label {
  bottom: 0;
}

.running-band.is-footer .running-band-label {
  top: 0;
}

/* ---- Header / footer marker -------------------------------------------------- */
/* LibreOffice Writer's affordance (docs/85 §8d): a marker appears when the
   pointer is in a margin band, so an ABSENT header is discoverable — a bare
   double-click is invisible to anyone who has not been told about it. Positioned
   over the page in the overlay layer, never composited into the raster. */
.running-marker {
  position: absolute;
  z-index: 4;
  padding: 3px 9px;
  border: 1px solid var(--accent-text);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--accent-text);
  font: inherit;
  font-size: var(--fs-small);
  line-height: 1.4;
  cursor: pointer;
  box-shadow: 0 1px 3px rgb(0 0 0 / 18%);
}

.running-marker:hover {
  background: var(--accent);
  color: var(--accent-ink);
}

.running-marker:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
}

.running-marker[hidden] {
  display: none;
}

/* ---- ONE select treatment for the whole chrome (docs/114 §6) ---------------- */
/* Before this, `.ctl select` alone was restyled (`appearance: none` plus the
   product's own caret) while `.dialog-select`, `.menu-select` and `.save-format`
   rendered raw OS chrome — so the native selects were not even consistent with each
   other, and one of them sat in the ribbon band directly above a custom gallery.
   The band now has NO `<select>` at all; what is left is in dialogs and menu rows,
   where a platform popup over a long list is the right affordance. Those wear the
   product's field and the product's caret.

   Placed last on purpose: `.ctl select` sets the `background` SHORTHAND earlier in
   this file, which resets `background-image`. Same specificity, later rule wins.
   `tests/select_chrome.test.mjs` fails the build if a `<select>` in editor.html is
   not covered by this selector list, and `styles-control.spec.mjs` re-checks it from
   COMPUTED style, so a rule that is present but overridden still fails.

   The caret is a data-URI SVG. Its `%236a7078` is the palette's `--muted`, inlined
   because a url() cannot read a custom property; `style_tokens.test.mjs` scans for
   `#rrggbb` literals, which this is not, and this grey is legible in both themes. */
.ctl select,
.dialog-select,
.dialog-field select,
.menu-select,
.save-format {
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%236a7078'><path d='M7 10l5 5 5-5z'/></svg>");
  background-repeat: no-repeat;
  background-position: right 5px center;
  background-size: 16px;
  cursor: pointer;
}

/* `.menu-select` was `padding: 0 6px`, which would put its text under the caret. */
.menu-select {
  padding: 0 24px 0 8px;
}

/* ---- File page: the content pane's own furniture ---------------------------
 * ONLYOFFICE's Save As pane is a grid of format tiles (`filemenu.less`
 * `.format-items`). This is that grid, at our type scale. */
.file-format-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
  gap: 10px;
  max-width: 720px;
}

.file-format-tile {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--bg);
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.file-format-tile:hover:not(:disabled),
.file-format-tile:focus-visible {
  border-color: var(--accent, var(--line));
  background: var(--surface);
}

.file-format-tile:disabled {
  opacity: 0.5;
  cursor: default;
}

/* 28px is ONLYOFFICE's own `@x-huge-btn-icon-size` — the size they give the
   icon on a tall tile, which is what these are. */
.file-format-icon {
  font-size: 28px;
  line-height: 1;
  color: var(--ink-soft, var(--ink));
  opacity: 0.85;
}

.file-format-tile:hover:not(:disabled) .file-format-icon {
  opacity: 1;
}

.file-format-ext {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 42px;
  padding: 4px 6px;
  border-radius: 4px;
  background: var(--surface-2, var(--surface));
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--ink-soft, var(--ink));
}

/* A category row points INTO the page; the chevron says so, as theirs does. */
.file-page-item-chevron {
  margin-left: auto;
  opacity: 0.55;
  font-size: 18px;
}

.file-page-item-category[aria-pressed="true"] {
  background: var(--surface);
  font-weight: 600;
}

.panel-in-page {
  width: 100%;
  max-width: none;
  min-width: 0;
}

/* The popup CARD inside an overlay-style dialog — the palette's box, a
   dialog's inner panel — flattens for the same reason. */
.panel-in-page > .cmd-box,
.panel-in-page > .cmd-panel,
.panel-in-page > .dialog,
.panel-in-page > .dialog-card,
.panel-in-page > .dialog-panel,
.panel-in-page > .settings-inner {
  width: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}

/* A pane's own heading is the page's; a panel that carries its own title
   would print it twice. */
/* The pane supplies the heading and the measure beneath it, so the panel's own
   dialog header is a SECOND title saying the same thing — and its close button
   would close a dialog that is not open. `>` was wrong here: the header sits
   inside the dialog's card, not at the panel's root, so the duplicate title
   showed on every borrowed panel. */
.panel-in-page .dialog-head,
.panel-in-page .dialog-title,
.panel-in-page > h2:first-child {
  display: none;
}

/* A popup's list is capped so the popup fits the screen. In a pane there is no
   popup to fit — the pane itself scrolls — so the cap only cut the list off
   mid-row, which is what made this read as a popup someone had parked here. */
.panel-in-page .cmd-list,
.panel-in-page .shortcuts-body,
.panel-in-page [class$="-list"] {
  max-height: none;
  overflow: visible;
}

/* The search field fills the measure rather than sitting in a popup. */
.panel-in-page input[type="text"],
.panel-in-page input[type="search"] {
  width: 100%;
  box-sizing: border-box;
}

/* A dialog shown as a File-page PANE: it is a section of the page now, so it
   drops the overlay's frame and scrolls with the pane rather than scrolling
   the window behind it (`docs/123`; the owner's report was the Settings form
   being taller than the window). */
.panel-in-page,
.dialog-overlay.panel-in-page {
  position: static;
  display: block;
  inset: auto;
  width: auto;
  max-width: var(--file-page-measure);
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
  backdrop-filter: none;
  overflow: visible;
}

/* The inner card a dialog-overlay wraps loses its own frame too. */
.panel-in-page > .dialog,
.panel-in-page > .dialog-card,
.panel-in-page > .dialog-panel {
  position: static;
  width: auto;
  max-width: none;
  max-height: none;
  margin: 0;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
}

/* The dialog's own close button is meaningless in a pane — the rail is how you
   leave, and `Back to document` is how you leave the page. */
.panel-in-page .dialog-close { display: none; }

/* A dialog card shrink-wraps its content and centres itself in the overlay —
   right for a dialog, wrong for a pane: About's facts sat adrift in the middle
   of the page while every other pane started at the heading's left edge, and
   the card's own body padding indented the rest by a further 22px. In the pane
   the card IS the measure, so it takes the pane's width and its left edge.
   Scoped to `#filePageDetail` so it outranks each dialog's own sizing without
   having to name them one at a time. */
#filePageDetail .panel-in-page > .dialog-card,
#filePageDetail .panel-in-page > .cmd-box {
  width: 100%;
  max-width: none;
  margin: 0;
}

/* The FOOT takes the same measure as the body, for the same reason the body
   takes it: in a pane the card is the page, so nothing in it may start 22px in
   from where the pane's heading starts. The rule above did this for the body
   when the panes landed and left the action row behind, and the gap stayed
   invisible for as long as some text in the body happened to sit at the page's
   left edge — every pane had one, so the "same left edge" guard was measuring
   the body and reporting on the card. Document properties' two columns took
   that text out of the first column and the footer's own inset surfaced:
   heading at 344, body at 344, "Changes apply as one undoable action." at 366.
   It is one inset for the whole card now. */
#filePageDetail .panel-in-page .dialog-foot {
  padding-left: 0;
  padding-right: 0;
}

#filePageDetail .panel-in-page .dialog-body {
  padding-left: 0;
  padding-right: 0;
  /* The dialog body's scroll shadows come with an opaque `--surface` beneath
     them, which is right for a card floating on a scrim and wrong for a pane:
     the pane does not scroll (the page does), so the shades would never fire,
     and the fill would print a white block across the File page. */
  background: none;
  /* ...and for the same reason it must stop being a SCROLL CONTAINER here.
     As a modal body it is `overflow-y: auto` with `overscroll-behavior: contain`,
     which is correct over a scrim. Borrowed into the pane it becomes a container
     with nothing to scroll — its content fits it exactly — and `contain` on a
     non-scrollable element BLOCKS scroll chaining in Chromium. So the wheel died
     two levels below the only element that could move:

         .dialog-body     auto / contain   scrollable: false   <- swallowed here
         .dialog-card     hidden           scrollable: false
         #filePageDetail  auto / contain   scrollable: TRUE    <- never reached

     Every File-page pane was therefore unscrollable by wheel while its content
     ran past the fold — Settings had 877px of content in 662px of pane. */
  overflow: visible;
  overscroll-behavior: auto;
  max-height: none;
}

/* The card clips too: `overflow: hidden` keeps a floating card's corners clean,
   but in the pane it is a second lid over content the page is trying to scroll. */
#filePageDetail .panel-in-page > .dialog-card {
  overflow: visible;
  max-height: none;
}

/* Template cards are taller than export tiles because a page miniature sits on
   top of the name, the way Google Docs presents its template gallery. */
.file-template-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(156px, 1fr));
  gap: 16px;
  max-width: 720px;
}

.file-template-tile {
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 14px 12px 12px;
}

.file-template-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  align-items: center;
  text-align: center;
}

.file-template-description {
  font-size: 12px;
  color: var(--ink-soft, var(--ink));
  opacity: 0.7;
}

/* The preview is a real Letter sheet drawn at its real point sizes and scaled
   down as one block, so `.file-template-page` fixes the visible box and the
   sheet inside it overflows into that scale. */
.file-template-page {
  display: block;
  position: relative;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 3px;
  /* Paper, in both themes: this is a picture of a page, and a page is white
     even when the chrome around it is dark — the same reason `--paper` is a
     fixed light value rather than a themed surface. */
  background: var(--paper);
  box-shadow: 0 1px 3px rgb(0 0 0 / 12%);
  flex: none;
}

.file-template-sheet {
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: top left;
  box-sizing: border-box;
  background: var(--paper);
  color: var(--paper-ink);
  font-family: var(--doc-font, "Times New Roman", Times, serif);
  text-align: left;
}

.file-template-line {
  display: block;
  line-height: 1.08;
  white-space: nowrap;
  overflow: hidden;
}

.file-template-tile:hover:not(:disabled) .file-template-page,
.file-template-tile:focus-visible .file-template-page {
  border-color: var(--accent, var(--line));
}

/* ---- Region composition (docs/126 phase 3, ADR-038) ------------------------
   A host chooses which chrome its container paints, and a region a role has no
   business with is ABSENT, not dimmed. `docs/126`'s container policy draws the
   line: "never, for you" is composition and is silent; "not right now" is state
   and explains itself with a reason. A `readonly` container has no editing
   ribbon — not a ribbon full of greyed buttons, no ribbon — which is how Word,
   Google Docs and ONLYOFFICE all present read-only.

   Body classes, because the editor toggles `hidden` on most of these elements as
   part of working (the rail flips `#outlinePanel.hidden`, every tab change flips
   a panel's), so a region removed by setting `hidden` would come back the first
   time its feature ran. This is the same shape `body:not(.doc-loaded)` above
   already uses to hide the ribbon, the rail and the status bar together.
   `chrome_regions.mjs` sets the classes; `capabilities.mjs` decides the set. */

body.chrome-no-brand .brand,
body.chrome-no-title .document-title-row,
/* The document-STATE cluster: the saved/opened chip and the import-compatibility
   count in header row 1, and Document properties in row 2. They report what the
   document IS rather than offering a way to change it, which is why they are one
   region across two rows rather than two. */
body.chrome-no-state .controls-doc,
body.chrome-no-state #propertiesBtn,
body.chrome-no-ribbon .ribbon,
body.chrome-no-ribbon .ribbon-nav,
body.chrome-no-rail .rail,
body.chrome-no-rail #outlinePanel,
body.chrome-no-rail #pagesPanel,
/* Version history, withheld: the panel, its two entry points and its preview bar.
   The COMMAND is withheld too, in `main.js`, because the palette and the ⌘⌥⇧H
   chord belong to no region and CSS cannot reach them. */
body.chrome-no-history #versionPanel,
body.chrome-no-history #viewVersionsBtn,
body.chrome-no-history #railVersions,
body.chrome-no-history #versionPreviewBanner,
/* The review-mode switcher (ONLYOFFICE `customization.review.hideReviewDisplay`).
   A host whose own product decides who is reviewing does not want a switch
   offered; the GRANT still decides what the editor does, so taking the control
   away neither widens nor narrows anything. */
body.chrome-no-review #reviewModeControl,
/* …and its second face at the top right, and the menu that face opens. A region
   that took the status-bar switch away and left the header one on screen would
   be a host believing they had withheld a surface that is still there. */
body.chrome-no-review #headerModeControl,
body.chrome-no-review #headerModeMenu,
body.chrome-no-status .footer,
/* The toast is the status line's OTHER channel — body-level, so it survives the
   footer being taken away below 620px. A container that withheld `status`
   withheld the message, not one of the two places it lands. */
body.chrome-no-status #statusToast,
body.chrome-no-zoom .zoom,
body.chrome-no-find #findPanel,
body.chrome-no-selection #selToolbar,
/* The four regions INSIDE the work area (the owner's preview report). Withheld
   means unreachable, not merely unpainted — `main.js` refuses the gesture at each
   entry point — so these rules are the second half of that, not the whole of it. */
body.chrome-no-ruler .ruler,
body.chrome-no-caret .caret,
body.chrome-no-caret .highlight,
body.chrome-no-context .editor-context-menu,
body.chrome-no-objects .object-outline,
body.chrome-no-objects .object-handle,
body.chrome-no-objects .object-bar-actions,
body.chrome-no-settings #settingsBtn,
body.chrome-no-settings #settingsPanel,
body.chrome-no-band-file #tabFile,
body.chrome-no-band-file #panelFile,
body.chrome-no-band-home #tabHome,
body.chrome-no-band-home #panelHome,
body.chrome-no-band-insert #tabInsert,
body.chrome-no-band-insert #panelInsert,
body.chrome-no-band-layout #tabLayout,
body.chrome-no-band-layout #panelLayout,
body.chrome-no-band-references #tabReferences,
body.chrome-no-band-references #panelReferences,
body.chrome-no-band-review #tabReview,
body.chrome-no-band-review #panelReview,
body.chrome-no-band-view #tabView,
body.chrome-no-band-view #panelView,
body.chrome-no-band-table #tabTable,
body.chrome-no-band-table #panelTable {
  display: none !important;
}

/* The menu bar is the OTHER navigation axis, and the one-axis invariant (`109`
   UX-014, docs/122) says the two are never on screen together.
   `body.ribbon-mode #appMenuBar { display: none }` already enforces that for the
   default chrome, so withholding the ribbon has to REVEAL the menu bar or a
   reading container would have no way to reach File > Print — which is exactly
   the capability `readonly` is granted. One class more specific than the
   ribbon-mode rule, so it wins. */
body.chrome-no-ribbon.ribbon-mode #appMenuBar {
  display: flex;
}

/* And a container with no menu bar either — `preview`, the runtime as a rendering
   engine — gets neither axis, because it has no commands to reach. */
body.chrome-no-menu #appMenuBar,
body.chrome-no-menu.ribbon-mode #appMenuBar,
body.chrome-no-menu #appMenusBtn,
body.chrome-no-menu #appMenuPopover {
  display: none !important;
}

/* The chrome-mode switch chooses between the two axes, so it is meaningless in a
   container that was given one of them and not the other. */
body.chrome-no-ribbon .chrome-mode,
body.chrome-no-menu .chrome-mode {
  display: none;
}

/* A container with no caret has no text selection either — and the browser's own
   selection is the half a `display: none` rule cannot reach. `preview` is the
   runtime as a rendering engine: "it should be just pages.. and nothing". */
body.chrome-no-caret .pages,
body.chrome-no-caret .page-wrap {
  user-select: none;
  -webkit-user-select: none;
}

/* And when every region the top bar paints has been withheld, the bar itself goes
   — otherwise a `preview` container, which composes ALL chrome away, still pays
   58px for an empty strip above the document it exists to show. The selector IS
   the bar's contents: brand, title, the state cluster, the menu bar, the ribbon's
   tab strip, Settings and the editing-mode selector (`review`, which joined the
   bar with `109` UX-025). It is stated as a conjunction rather than as a separate
   `no-bar` flag so there is no second mechanism to keep in step with the region
   set — `chrome_regions.test.mjs` holds this list against the ids `.bar` really
   contains, so a control added to the header without a region fails the build
   instead of quietly re-inflating the strip. */
body.chrome-no-brand.chrome-no-title.chrome-no-state.chrome-no-menu.chrome-no-ribbon.chrome-no-settings.chrome-no-review
  .bar {
  display: none;
}

/* ---- Chart surfaces: the Chart Data dialog (`chart_data.mjs`), the Chart
 * settings panel (`chart_panel.mjs`) and the Chart tab's style gallery
 * (`chart_surface.mjs`). ONLYOFFICE's split: data in its own window, settings
 * at the right, the style gallery on the tab. Every colour is a token; the
 * swatch dots are the one exception, painted the document's own theme colours
 * the engine resolved, because a swatch has to show what the chart will be. */
.side-panel.chart-panel {
  flex-basis: 320px;
  width: 320px;
  max-width: min(320px, calc(100vw - 52px));
}

.chart-panel-section select,
.chart-panel-section input[type="text"] {
  width: 100%;
}

.chart-panel-checks {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.chart-panel-check {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 24px;
  color: var(--ink);
  font-size: var(--fs-small);
  cursor: pointer;
}

.chart-panel-summary {
  margin: 0 0 var(--space-2);
  color: var(--muted);
  font-size: var(--fs-small);
}

.chart-data-overlay .chart-data-card {
  width: min(720px, calc(100vw - 32px));
  max-width: none;
}

.chart-data-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.chart-data-reason {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--bg-2);
  color: var(--ink);
  font-size: var(--fs-small);
  line-height: 1.45;
}

.chart-palette-grid,
.chart-ribbon-styles {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.chart-ribbon-styles {
  flex-wrap: nowrap;
  align-items: center;
  gap: 4px;
}

.chart-palette {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  min-width: 24px;
  min-height: 24px;
  padding: 5px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
}

.chart-palette:hover:not(:disabled) {
  border-color: var(--line-strong);
  background: var(--bg-2);
}

.chart-palette[aria-checked="true"] {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.chart-palette:disabled {
  cursor: default;
  opacity: 0.6;
}

.chart-palette-dot {
  display: inline-block;
  width: 10px;
  height: 14px;
  border-radius: 1px;
}

.chart-palette-ribbon .chart-palette-dot {
  width: 6px;
  height: 18px;
}

/* Chart formatting sections: number format, colour well, font toggles. */
.chart-numfmt {
  display: grid;
  gap: var(--space-1);
}

.chart-color-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.chart-color-row input[type="color"] {
  width: 40px;
  min-height: 28px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
}

.chart-font-toggles {
  display: flex;
  gap: var(--space-1);
  margin-top: var(--space-2);
}

.chart-font-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 28px;
  min-height: 28px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
}

.chart-font-toggle:hover:not(:disabled) {
  background: var(--bg-2);
}

.chart-font-toggle[aria-pressed="true"] {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.chart-font-toggle:disabled,
.chart-color-row input:disabled {
  cursor: default;
  opacity: 0.6;
}

.chart-grid-scroll {
  max-height: 52vh;
  overflow: auto;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  overscroll-behavior: contain;
}

.chart-grid {
  border-collapse: collapse;
  font-size: var(--fs-small);
}

.chart-grid th,
.chart-grid td {
  padding: 0;
  border-right: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}

.chart-grid thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--bg-2);
}

.chart-grid tbody th {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--bg-2);
}

.chart-grid thead th:first-child {
  left: 0;
  z-index: 2;
}

.chart-grid-corner {
  padding: 0 6px;
  color: var(--muted);
  font-weight: 600;
  text-align: start;
  white-space: nowrap;
}

.chart-grid input {
  width: 96px;
  min-height: 28px;
  padding: 0 6px;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--ink);
  font: inherit;
}

.chart-grid th input {
  font-weight: 600;
}

.chart-grid input.chart-cell-number {
  text-align: end;
  font-variant-numeric: tabular-nums;
}

.chart-grid input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.chart-grid input[aria-invalid="true"] {
  outline: 2px solid var(--danger);
  outline-offset: -2px;
}

.chart-grid input:read-only {
  color: var(--muted);
}

.chart-grid-head {
  display: flex;
  align-items: center;
}

.chart-grid-btn {
  display: inline-grid;
  place-items: center;
  flex: 0 0 24px;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

.chart-grid-btn .ms {
  font-size: 16px;
}

.chart-grid-btn:hover:not(:disabled) {
  background: var(--bg-2);
  color: var(--danger);
}

.chart-grid-btn:disabled {
  cursor: default;
  opacity: 0.35;
}

.chart-grid-tail {
  padding: 0 2px;
}

.chart-grid-actions {
  display: flex;
  gap: var(--space-2);
}

.chart-data-note {
  min-height: 1.45em;
  margin: 0;
  color: var(--muted);
  font-size: var(--fs-small);
  line-height: 1.45;
}

.chart-data-note.error {
  color: var(--danger);
}

/* Remote awareness never intercepts document interaction. */
.remote-caret { position: absolute; border-left: 2px solid; pointer-events: none; z-index: 6; }
.remote-caret > span { position: absolute; bottom: 100%; left: -2px; color: white; font: 11px sans-serif; padding: 2px 4px; white-space: nowrap; border-radius: 3px; }
.remote-selection { position: absolute; opacity: .18; pointer-events: none; z-index: 5; }

.shared-link-dialog { max-width: min(560px, 90vw); border: 1px solid var(--line); border-radius: 8px; padding: 24px; }
.shared-link-dialog label { display: block; }
.shared-link-dialog input { display: block; width: 100%; margin: 12px 0; }
