/* Editor-scale geometry. Named with the --editor-tile-* prefix to make the
   boundary between chrome ladder (buttons, inputs, strips — see main.css) and
   editor reading-rhythm ladder explicit. Consumers live in editor-core only;
   app / keaiui do not read these. See docs/claude-ui-size-system.md §3.4. */
.editor-view-root {
  --editor-grid-gap-x: 2px;
  --editor-grid-gap-y: 2px;
  --editor-child-grid-indent: 12px;
  --editor-child-grid-gap-top: 4px;
  --editor-tile-min-width: 29px;
  --editor-tile-min-height: 35px;
  --editor-tile-padding: 5px;
  --editor-tile-radius: 4px;
  --editor-tile-border-color: var(--color-border);
  --editor-tile-content-padding: 1px;
  --editor-tile-max-text-width: 24rem;
  --editor-tile-font-size: 14px;
  --editor-tile-line-height: 1.3;
  --editor-tile-todo-toggle-size: var(--icon-md);
  --editor-tile-todo-toggle-gap: var(--space-4);
  --editor-tile-link-label-width: 28px;
  --editor-tile-type-actions-height: 24px;
  --editor-tile-rich-preview-min-height: calc(var(--editor-tile-font-size) * var(--editor-tile-line-height));
}

.editor-view-grid,
.tile-children-table {
  border-collapse: separate;
  border-spacing: var(--editor-grid-gap-x) var(--editor-grid-gap-y);
  width: max-content;
  table-layout: auto;
  outline: none;
  display: table;
}

.editor-view-grid > tbody > tr > td,
.tile-children-table > tbody > tr > td {
  vertical-align: top;
}

/* A measured grid reserves its complete footprint around native cell tables.
   Inheriting these properties keeps the owning grid's spacing/borders intact. */
.grid-window-table {
  position: absolute;
  border-collapse: inherit;
  border-spacing: inherit;
  table-layout: fixed;
}

/* Neighboring tables paint collapsed borders separately. Keep each cell's
   inward outline above those borders, as it is in a shared native table. */
.grid-window-table > tbody > tr > .tile {
  z-index: 1;
}

.tile {
  position: relative;
  width: max-content;
  min-width: var(--editor-tile-min-width);
  min-height: var(--editor-tile-min-height);
  padding: var(--editor-tile-padding);
  padding-block: var(--editor-tile-padding-block, var(--editor-tile-padding));
  vertical-align: top;
  border: 1px solid var(--tile-border_color, var(--editor-tile-border-color));
  border-radius: var(--editor-tile-radius);
  /* Every tile-style override is reset to `initial` here so none of them cascade
     into child tiles — a style only ever affects its own tile. `initial` makes
     each custom property guaranteed-invalid, so the var() fallback (the design
     default) applies unless the renderer sets an inline --tile-* on this tile.
     Resetting per-tile is what blocks inheritance (custom props inherit by
     default). */
  --tile-background_color: initial;
  --tile-color: initial;
  --tile-fill-ink: initial;
  --tile-reading-fill: initial;
  --tile-state-backing: initial;
  --tile-wash-strength: 100%;
  /* Flat content roles stay local, including authored foreground precedence. */
  --tile-local-ink: var(--tile-color, var(--editor-document-color, var(--tile-fill-ink)));
  /* Keep mixes flat: nested color-mix outline values crash macOS WebKit.
     Missing fill ink invalidates this local recipe; each consumer falls back
     to its ordinary theme ring. */
  --tile-nav-ring: color-mix(in srgb, var(--tile-fill-ink) 95%, var(--mode-navigate-color));
  --tile-edit-ring: color-mix(in srgb, var(--tile-fill-ink) 95%, var(--mode-edit-color));
  --tile-range-ring: color-mix(in srgb, var(--tile-fill-ink) 95%, var(--tile-selection-color));
  --tile-search-ring: color-mix(in srgb, var(--tile-fill-ink) 95%, var(--color-search-match));
  --tile-border_color: initial;
  --tile-outline_width: initial;
  --tile-font_family: initial;
  --tile-font_size: initial;
  --tile-text_align: initial;
  --tile-font_weight: initial;
  --tile-font_style: initial;
  --tile-text_underline: initial;
  --tile-text_strike: initial;
  background-color: var(--tile-background_color, var(--color-surface-page));
  /* Emphasis outline: thickness is the per-tile knob (default 0 = no outline),
     colored by the tile's border color (decision: one color drives both border
     and outline). `outline` is layout-neutral, so changing it never shifts grid
     layout — unlike a border-width change, would. Pulled INWARD by its own width
     (negative offset) so the ring sits flush inside the border, not bleeding
     over neighbors. These base outline vars reset on every tile. Current-mode
     changes use non-inherited outline properties directly, so entering Edit on
     a parent does not invalidate custom properties throughout its subtree. */
  --tile-outline-color: var(--tile-border_color, var(--editor-tile-border-color));
  --tile-outline-w: var(--tile-outline_width, 0px);
  outline: var(--tile-outline-w) solid var(--tile-outline-color);
  outline-offset: calc(-1 * var(--tile-outline-w));
  box-sizing: border-box;
  /* Layout containment: an edit inside one tile can't drag the rest of the grid
     into its layout pass. NOT size containment — the tile must still size to its
     content for the max-content nested-table grid to resolve. */
  contain: layout;
  /* Leave room for the mode cue when keyboard navigation scrolls a tile in. */
  scroll-margin-block-start: var(--space-7);
  font-family: var(--tile-font_family, var(--editor-document-font-family, var(--font-sans)));
  /* No `text-rendering: optimizeLegibility` here: it forces kerning/ligature
     shaping on every tile (potentially thousands), which is a first-paint and
     reflow cost. Default `auto` shapes lazily and reads identically at UI sizes. */
  text-wrap: normal;
  font-size: var(--tile-font_size, var(--editor-tile-font-size));
  line-height: var(--editor-tile-line-height);
  color: var(--tile-local-ink, var(--color-text-strong));
  text-align: var(--tile-text_align, left);
}

/* Depth ladder: 0.5px per level (was 1px — it shrank too fast; by depth 4
   text was 71% of base). Gentle enough to keep nesting legible while still
   cueing hierarchy. */
.tile[data-depth="1"] {
  font-size: var(--tile-font_size, calc(var(--editor-tile-font-size) - 0.5px));
}

.tile[data-depth="2"] {
  font-size: var(--tile-font_size, calc(var(--editor-tile-font-size) - 1px));
}

.tile[data-depth="3"] {
  font-size: var(--tile-font_size, calc(var(--editor-tile-font-size) - 1.5px));
}

.tile[data-depth="4"] {
  font-size: var(--tile-font_size, calc(var(--editor-tile-font-size) - 2px));
}

/* The current tile reuses its OWN outline as the selection ring, recolored to
   the selection color — so a decorative outline and the selection indicator are
   one ring, never two crowding each other. When no decorative outline is set,
   fall back to a 2px ring so selection stays visible. The second selector beats
   the grid-mode cell rule below (which is wrapped in :where() to keep its
   specificity low) so the current cell recolors in grid mode too. */
.tile[data-current-tile="true"] {
  outline-color: var(--tile-range-ring, var(--tile-selection-color));
  outline-width: max(2px, var(--tile-outline_width, 0px));
  outline-offset: calc(-1 * max(2px, var(--tile-outline_width, 0px)));
}

/* Search matches reuse the SAME single outline (never a second ring): a match
   that is not the current tile gets the ring in the search-blue at the focus
   width; a match that IS the current tile keeps the selection ring above (the
   :not() yields to it). The ::before overlay fill applies on top in both cases. */
.tile.search--match:not([data-current-tile="true"]) {
  --tile-outline-color: var(--tile-search-ring, color-mix(in srgb, var(--color-search-match) 60%, var(--color-text-strong)));
  --tile-outline-w: max(2px, var(--tile-outline_width, 0px));
}

.tile.search--match::before {
  content: "";
  opacity: 1;
  --tile-wash: color-mix(in srgb, var(--color-search-match) 22%, transparent);
}

.tile.search--match.search--match--selected::before {
  opacity: 1;
  --tile-wash: color-mix(in srgb, var(--color-search-match) 40%, transparent);
}

/* Bubble selections keep each tile's ring and tint. Grid selections add a
   shared selection fill and rectangle perimeter below; document borders
   remain their own style. */
.tile[data-selected="true"]:not([data-selection-edges]) {
  --tile-outline-color: var(--tile-range-ring, var(--tile-selection-color));
  --tile-outline-w: max(2px, var(--tile-outline_width, 0px));
}

/* Only highlighted cells need an overlay box. Invisible overlays on every
   idle cell add layout/pre-paint work when a large table changes column width. */
.tile::before {
  content: none;
  position: absolute;
  inset: 1px;
  border-radius: calc(var(--editor-tile-radius) - 1px);
  pointer-events: none;
  opacity: 0;
  --tile-wash: var(--selection-fill);
  background: linear-gradient(color-mix(in srgb, var(--tile-wash) var(--tile-wash-strength), transparent), color-mix(in srgb, var(--tile-wash) var(--tile-wash-strength), transparent)), var(--tile-state-backing, transparent);
}

.tile[data-selected="true"]::before {
  content: "";
  opacity: 1;
  /* Stronger than the faint --selection-fill tint so a non-focus selected tile
     is clearly filled, not barely distinguishable (F2). */
  --tile-wash: var(--tile-selection-fill);
}

/* The focus cell within a multi-tile selection reads brighter, so "where the
   cursor is" stays obvious even though the whole block is now ringed + filled. */
.tile[data-selected="true"][data-current-tile="true"]::before {
  --tile-wash: color-mix(in srgb, var(--tile-selection-color) 24%, transparent);
}

/* The default current-cell tint also covers views without keyboard modes.
   Navigate and Edit below give the interactive view distinct treatments. */
.tile[data-current-tile="true"]::before {
  content: "";
  opacity: 1;
  --tile-wash: color-mix(in srgb, var(--selection-border) 9%, transparent);
}

/* Edit overlay (frozen box): while editing, the tile keeps its entry-size
   footprint (inline width/height set by tile_focus) and its content overflows
   freely ABOVE the neighbors — the grid reflows only when the edit commits.
   The content element's own box still grows with the text, so giving it the
   tile background + a lift shadow makes the overflow read as an editing card. */
/* Let the absolute draft contribute its overflow to the scrollable canvas;
   layout containment otherwise strands long drafts beyond the scroll limit.
   Lift the ancestor chain and positioned window tables above their siblings. */
.grid-window-table:has(.tile[data-edit-frozen="true"]),
.tile[data-edit-frozen="true"],
.tile:has(.tile[data-edit-frozen="true"]) {
  contain: none;
  overflow: visible;
  z-index: 40;
}

.tile[data-edit-frozen="true"] > .tile_content {
  position: relative;
  z-index: 1;
  /* Grow rightward with typing (like a normal tile) up to the same
     max-text-width ceiling; JS pins min-width/min-height at the entry size. */
  width: max-content;
  background-color: var(--tile-reading-fill, var(--tile-background_color, var(--color-surface-page)));
  box-shadow: 0 3px 14px color-mix(in srgb, var(--color-text) 35%, transparent);
  border-radius: var(--radius-1);
}

/* The parked editor root (navigate mode holds DOM focus there) is a focus
   holder, not a control — the tile cursor is the visible state. Keyboard-
   driven .focus() marks the root :focus-visible, which otherwise paints the
   UA's blue ring around the whole grid (user report 2026-07-16). */
[data-editor-mode]:focus-visible {
  outline: none;
}

/* Mode styling stays on the current tile and its cue. The root mode attribute
   remains the keyboard/app API, without invalidating the whole grid's styles. */
.tile[data-tile-mode="edit"]:not([data-selected="true"]) {
  outline-color: var(--tile-edit-ring, var(--mode-edit-border));
}
.tile[data-tile-mode="edit"]:not([data-selected="true"])::before {
  --tile-wash: color-mix(in srgb, var(--mode-edit-color) 12%, transparent);
}
.tile[data-tile-mode="navigate"]:not([data-selected="true"]) {
  outline-color: var(--tile-nav-ring, var(--mode-navigate-border));
  outline-width: max(3px, var(--tile-outline_width, 0px));
  outline-offset: calc(-1 * max(3px, var(--tile-outline_width, 0px)));
}

.tile[data-tile-mode="navigate"]:not([data-selected="true"])::before {
  --tile-wash: color-mix(in srgb, var(--mode-navigate-color) 18%, transparent);
}

/* A current tile may be inside a selected ancestor after a cross-grid range.
   Keep its identity/mode cue while returning its paint to document styling. */
.tile[data-selection-edges][data-selected="true"],
.tile[data-selection-edges] .tile[data-current-tile="true"] {
  outline-color: var(--tile-outline-color);
  outline-width: var(--tile-outline-w);
  outline-offset: calc(-1 * var(--tile-outline-w));
}

.tile[data-selection-edges] .tile[data-current-tile="true"]::before {
  opacity: 0;
}

.tile[data-selection-edges][data-selected="true"]::before {
  opacity: 1;
  --tile-wash: var(--tile-selection-fill);
  inset: 0;
  border-radius: 0;
}

.tile[data-selection-edges]::after {
  content: "";
  position: absolute;
  /* Overlap the shared gridline so adjacent perimeter segments have no gaps. */
  inset: -1px;
  border: 0 solid var(--tile-range-ring, var(--tile-selection-color));
  pointer-events: none;
  z-index: 1;
}

.tile[data-selection-edges~="top"]::after {
  border-top-width: 2px;
}

.tile[data-selection-edges~="right"]::after {
  border-right-width: 2px;
}

.tile[data-selection-edges~="bottom"]::after {
  border-bottom-width: 2px;
}

.tile[data-selection-edges~="left"]::after {
  border-left-width: 2px;
}

/* Lift the active tile and its ancestors so both mode cues clear neighboring
   cells in nested grids. This is independent of edit freezing. */
.tile:has(.tile_mode_cue) {
  overflow: visible;
  z-index: 40;
}

/* Both cues have equal weight and follow the current tile. They sit outside
   contenteditable, add no grid size, and never intercept clicks. The shell's
   mode indicator provides the live accessibility announcement. */
.tile_mode_cue {
  display: none;
  position: absolute;
  bottom: calc(100% + var(--space-2));
  left: 0;
  z-index: 2;
  padding: 1px var(--space-2);
  align-items: center;
  gap: var(--space-2);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-2);
  background: var(--color-surface-raised);
  color: var(--color-text-strong);
  font: var(--text-ui-font);
  font-weight: 600;
  white-space: nowrap;
  pointer-events: none;
  user-select: none;
}

.tile_mode_cue[data-mode="navigate"],
:where(.tile:not([data-selected="true"])) > .tile_mode_cue[data-mode="edit"] {
  display: inline-flex;
}

.tile_mode_cue > svg {
  --action-icon-stroke: currentColor;
  --action-icon-fill-opacity: 0;
  flex: none;
  width: var(--icon-md);
  height: var(--icon-md);
}

.tile_mode_cue[data-mode="navigate"] > svg:last-child,
.tile_mode_cue[data-mode="edit"] > svg:first-child {
  display: none !important;
}

.tile_mode_cue[data-mode="navigate"] {
  border-color: var(--mode-navigate-border);
  background: var(--mode-navigate-face);
}

.tile_mode_cue[data-mode="navigate"]::after {
  content: "Navigate";
}

.tile_mode_cue[data-mode="edit"] {
  border-color: var(--mode-edit-border);
  background: var(--mode-edit-face);
  color: var(--color-text-strong);
}

.tile_mode_cue[data-mode="edit"]::after {
  content: "Edit · Esc to finish";
}

[data-mode-indicators="false"] .tile > .tile_mode_cue {
  display: none;
}

.editor-view-root.editor-view-print {
  display: none !important;
}

@media print {
  .editor-view-root.editor-view-print-source {
    display: none !important;
  }
  .editor-view-root.editor-view-print {
    display: block !important;
  }
  .tile_mode_cue,
  .tile_type_action_button,
  .tile_media_resize_handle,
  .tile[data-selection-edges]::after {
    display: none !important;
  }
  /* Paper cannot scroll a code preview. Preserve its width and source while
     letting long lines wrap and the complete block flow through the cell. */
  .tile_code_preview {
    max-height: none !important;
    overflow: visible !important;
    scrollbar-gutter: auto !important;
    white-space: pre-wrap !important;
    overflow-wrap: anywhere;
  }
  .tile_media_frame:has(.tile_code_preview) {
    overflow: visible !important;
  }
}

.tile[data-type="todo"] > .tile_content {
  padding-left: calc(
    var(--editor-tile-content-padding)
    + var(--editor-tile-todo-toggle-size)
    + var(--editor-tile-todo-toggle-gap)
  );
}

.tile[data-type="external-link"] > .tile_content {
  padding-left: calc(
    var(--editor-tile-content-padding)
    + var(--editor-tile-link-label-width)
    + var(--editor-tile-todo-toggle-gap)
  );
}

.tile[data-folded="true"] > .tile_children_block > .tile-children-table,
.tile[data-has-children="false"] > .tile_children_block > .tile-children-table {
  display: none;
}

.tile_fold_summary {
  background: var(--tile-reading-fill, transparent);
  display: inline-block;
  margin-left: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-size-ui);
  line-height: 1;
  color: var(--tile-local-ink, var(--color-text-muted));
  letter-spacing: 0.05em;
  text-transform: uppercase;
  cursor: pointer;
}

/* Folded: only the caret + "N × M hidden" chip remain in the row (the child
   table is hidden). Center them so the caret's tip points right at the chip. */
.tile[data-folded="true"] > .tile_children_block {
  align-items: center;
}

.tile_fold_summary:hover {
  color: var(--tile-local-ink, var(--color-text-strong));
}

.tile > .tile_content {
  display: block;
  min-width: 1ch;
  /* Chrome can omit an empty editable's line box when first laid out inert,
     and keep it collapsed after unlock. Reserve one line plus its padding on
     the content box; min-height on the surrounding table cell is ineffective. */
  box-sizing: border-box;
  min-height: calc(1lh + 2 * var(--editor-tile-content-padding));
  max-width: var(--editor-tile-max-text-width);
  margin: 0;
  padding: var(--editor-tile-content-padding);
  outline: none;
  position: relative;
  /* Keep the derived reading surface beneath the text. Selection/mode washes still
     tint the tile's surrounding padding and retain their outlines, without
     reducing contrast at the black/white fallback boundary. */
  background: var(--tile-reading-fill, transparent);
  color: inherit;
  font: inherit;
  font-weight: var(--tile-font_weight, normal);
  font-style: var(--tile-font_style, normal);
  /* Separate tokens let either line toggle without erasing the other. Empty
     fallbacks produce the initial `none` when both overrides are absent.
     Keep decorations on this content box so they never cross into children. */
  text-decoration-line: var(--tile-text_underline,) var(--tile-text_strike,);
  line-height: inherit;
  /* Pin both modern and legacy controls so ordinary words keep their intrinsic
     width in Safari's max-content table. `break-word` remains essential for an
     unbroken URL/token: it only breaks when the text would otherwise overflow. */
  overflow-wrap: break-word;
  word-break: normal;
  white-space: pre-wrap;
  cursor: text;
  caret-color: currentColor;
}

.tile_type_actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  opacity: 1;
  visibility: visible;
}

/* Editing actions reveal on hover or actual tile focus without moving the
   footer or hiding type labels. Touch keeps them visible. */
@media (hover: hover) {
  .tile_type_action_button {
    opacity: 0;
    pointer-events: none;
  }

  .tile:hover:not(:has(.tile:hover)) > .tile_content .tile_type_action_button,
  .editor-view-root:focus .tile[data-current-tile="true"] > .tile_content .tile_type_action_button,
  .tile > .tile_content:focus-within .tile_type_action_button,
  .tile_type_action_button:focus-visible {
    opacity: 1;
    pointer-events: auto;
  }
}

/* Clear is destructive — set it apart from the edit/copy/attach/link group so
   it's hard to hit by accident and lose work. */
.tile_type_clear_button,
.tile_media_clear_button {
  margin-left: var(--space-4);
}

/* Mini buttons (not link-style underlines) — same button grammar as the
   keaiui action buttons: raised bevel, button-face fill, compact. */
.tile_type_action_button {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 1px var(--space-2);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-1);
  background: var(--color-button-face);
  box-shadow: var(--bevel-raised);
  color: var(--color-text);
  font: var(--text-label-font);
  line-height: 1.5;
  cursor: pointer;
}

.tile_type_action_button_icon {
  display: inline-flex;
  width: 1em;
  height: 1em;
}

.tile_type_action_button:hover {
  background: var(--color-button-hover);
  color: var(--color-text-strong);
}

.tile_type_action_button:active {
  background: var(--color-button-active);
  box-shadow: var(--bevel-active);
}

.tile[data-type="code"] > .tile_content,
.tile[data-type="math"] > .tile_content,
.tile[data-type="diagram"] > .tile_content,
.tile[data-type="image"] > .tile_content,
.tile[data-type="audio"] > .tile_content,
.tile[data-type="video"] > .tile_content {
  display: flex;
  flex-direction: column;
  width: fit-content;
  max-width: min(36rem, 70vw);
  min-height: calc(var(--editor-tile-rich-preview-min-height) + var(--editor-tile-type-actions-height));
  cursor: default;
}

/* Resizable tiles fit their content's natural (or resized) size rather than the
   tight text cap — let them grow up to the viewport so a resized card shows
   full-size (and its corner handle stays on screen) instead of overflowing the
   tight cap and getting clipped. */
.tile[data-type="image"] > .tile_content,
.tile[data-type="audio"] > .tile_content,
.tile[data-type="video"] > .tile_content,
.tile[data-type="code"] > .tile_content,
.tile[data-type="math"] > .tile_content,
.tile[data-type="diagram"] > .tile_content {
  max-width: 90vw;
}

/* Background card + frame come from .tile_media_frame; the preview just fills it
   and keeps its own scroll/typography. */
.tile_code_preview {
  margin: 0;
  box-sizing: border-box;
  width: 100%;
  max-height: 10rem;
  overflow: auto;
  scrollbar-gutter: stable;
  white-space: pre;
  font: 12px/1.4 var(--font-mono);
  color: var(--color-text);
}

/* Shared preview/editor syntax palette. Both use Lezer classHighlighter and
   data-code-syntax so an editor cannot silently fall back to light-only colors.
   Light defaults + dark overrides keep
   readable contrast on the dark surface too (HIG §6 — code tiles previously
   rendered light-theme hex on a dark background). */
[data-code-syntax] .tok-keyword,
[data-code-syntax] .tok-modifier {
  color: var(--syntax-keyword);
}

[data-code-syntax] .tok-string,
[data-code-syntax] .tok-special.tok-string {
  color: var(--syntax-string);
}

[data-code-syntax] .tok-number,
[data-code-syntax] .tok-bool,
[data-code-syntax] .tok-null {
  color: var(--syntax-number);
}

[data-code-syntax] .tok-comment {
  color: var(--color-text-muted);
  font-style: italic;
}

[data-code-syntax] .tok-variableName.tok-definition,
[data-code-syntax] .tok-propertyName,
[data-code-syntax] .tok-labelName {
  color: var(--syntax-variable);
}

[data-code-syntax] .tok-typeName,
[data-code-syntax] .tok-className,
[data-code-syntax] .tok-tagName {
  color: var(--syntax-type);
}

[data-code-syntax] .tok-operator,
[data-code-syntax] .tok-punctuation,
[data-code-syntax] .tok-separator {
  color: var(--syntax-punctuation);
}

/* Math is short, intrinsically-sized content — let the card grow to fit it and
   only scroll an oversized formula, with no reserved gutter (which otherwise
   shows an empty scrollbar on first render). */
.tile_math_preview {
  box-sizing: border-box;
  width: 100%;
  /* Full natural height — KaTeX fractions/integrals are taller than a text line,
     and clipping them to the tile's line-height was forcing a vertical
     scrollbar even on the basic example. */
  overflow: visible;
  line-height: normal;
  /* Scale the formula with the card width (container units) so the resize handle
     actually grows/shrinks the math, not just the padding around it. Clamped so
     it stays legible at the extremes. */
  font-size: clamp(0.85rem, 10cqi, 2.4rem);
}

/* Only math frames are scaling containers (image/video/code keep their own
   sizing). */
.tile_media_frame:has(.tile_math_preview) {
  container-type: inline-size;
}

/* Plain container that holds the resizable frame (or, on error, the error
   affordance) and carries the media load-state hooks below. */
.tile_media_preview {
  max-width: 90vw;
}

.tile_image_preview,
.tile_audio_preview,
.tile_video_preview {
  display: flex;
  align-items: flex-start;
  justify-content: flex-start;
}

/* Audio isn't resizable (no frame), so it carries the bordered card itself. */
.tile_audio_preview {
  box-sizing: border-box;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-2);
}

.tile_media_image,
.tile_media_video {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-1);
}

/* Shared "preview card" wrapper for every resizable tile (image / video / code /
   math / diagram). This is what resizes — its width is the persisted
   type_data.width — and the content inside stretches to fill it. Bordered but
   unfilled: the frame distinguishes the card without a background tint. */
.tile_media_frame {
  position: relative;
  display: inline-block;
  box-sizing: border-box;
  min-height: var(--editor-tile-rich-preview-min-height);
  max-width: 90vw;
  overflow: hidden;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-2);
}

/* Syntax and generated diagram paint use the theme pair, independent of the
   surrounding authored fill. Filtering belongs to generated SVG, not its frame. */
.tile_media_frame:has(.tile_code_preview),
.tile_media_frame:has(.tile_diagram_preview),
.tile_placeholder {
  background: var(--color-surface-page);
  color: var(--color-text);
}

/* Muted corner-triangle handle shared with the type-editor resize grip
   (.type-editor-resize-handle in the ingrid app) so every resizable surface
   reads the same. Always visible (HIG §10.3 — tile chrome without hover-reveal)
   but faded so it recedes below the media + footer; firms up on hover. */
.tile_media_resize_handle {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 12px;
  height: 12px;
  /* A single short diagonal dash hugging the corner (offset off-center so it's
     the shorter segment, not the full main diagonal). The whole square stays
     hit-testable — background paint never affects pointer events — so clicks
     near the corner don't fall through to the media (e.g. a <video>'s controls). */
  background-image: linear-gradient(
    135deg,
    transparent calc(70% - 1px),
    var(--color-border-strong) calc(70% - 1px) calc(70% + 1px),
    transparent calc(70% + 1px)
  );
  opacity: 0.6;
  cursor: nwse-resize;
  transition: opacity 120ms ease;
}

.tile_media_frame:hover .tile_media_resize_handle {
  opacity: 0.9;
}

.tile_media_audio {
  display: block;
  max-width: 100%;
}

/* Empty "No X" box (media / diagram / code / math): glyph + title inline in a
   bordered box, clickable (fires the default action via the footer action class
   it carries). Solid border; dashed for the droppable ones (image/audio — their
   line carries the attach action class). The footer sits below it, unbordered. */
.tile_placeholder {
  /* Renders as a real <button> when interactive (see tile_placeholder.js) —
     reset the native chrome so it reads identically to the non-interactive div. */
  appearance: none;
  background: var(--color-surface-page);
  text-align: center;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  /* Fixed width + align-self:center so every type's empty box is the same size
     (independent of the footer width below it); filled tiles size to content. */
  width: 14rem;
  align-self: center;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-2);
  color: var(--color-text-muted);
  font: var(--text-ui-font);
  cursor: pointer;
}

.tile_placeholder.tile_media_attach_button {
  border-style: dashed;
}

.tile_placeholder[data-tone="error"] {
  border-color: var(--color-error);
}

.tile_placeholder:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

/* Glyph + "No X" on one line. */
.tile_placeholder_head {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}

/* Light second line, e.g. "Drop a file". */
.tile_placeholder_sub {
  font-size: var(--text-size-meta);
  color: var(--color-text-muted);
}

.tile_placeholder_icon {
  display: inline-flex;
  width: 1.5em;
  height: 1.5em;
  flex: 0 0 auto;
  /* Duotone type icon at rest: a muted-grey outline (stroke = currentColor)
     over the shared neutral light-grey fill (--tone-neutral) — the same grey
     the footer action icons and the settings gear use — so the empty state
     reads soft and consistent, not inky. */
  color: var(--color-text-muted);
  --action-icon-fill: var(--tone-neutral);
  opacity: 0.85;
}

.tile_placeholder_title {
  color: var(--color-text-strong);
  font-weight: 600;
}

.tile_placeholder[data-tone="error"] .tile_placeholder_title {
  color: var(--color-error);
}

.tile_media_preview[data-load-state="loading"] {
  opacity: 0.72;
}

.tile_media_preview[data-load-state="error"] {
  box-sizing: border-box;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-error);
  border-radius: var(--radius-2);
  background: var(--color-error-bg);
}

.tile_media_error {
  color: var(--color-error);
  font: var(--text-ui-font);
}

.tile_media_link {
  color: var(--color-info);
  font: var(--text-body-font);
  text-decoration: underline;
  text-underline-offset: 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

.tile_content[data-math-invalid="true"] .tile_math_preview {
  color: var(--tile-local-ink, var(--color-error));
  white-space: pre-wrap;
  font-family: var(--font-mono);
}

/* Leading type chip in a tile footer (type · [actions] · Clear). */
.tile_type_label {
  font-size: var(--text-size-label);
  color: var(--tile-local-ink, var(--color-text-muted));
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-right: var(--space-1);
}

.tile_type_footer {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-2);
  min-height: var(--editor-tile-type-actions-height);
  margin-top: auto;
  padding-top: var(--space-2);
}

/* Left-gutter slot, shared by the todo checkbox and the header H-level badge:
   absolute position + shared size. Sharing the slot is what guarantees both
   sit on the same vertical axis as the fold caret below (the fold bracket width
   is matched to this size). The todo skin (appearance reset, bevel, fill, check)
   comes from the global input[type="checkbox"] rule in the app CSS; the badge's
   text styling lives in .tile_header_indicator below. */
.tile_todo_toggle,
.tile_header_indicator,
.tile_external_link_edit_button {
  position: absolute;
  /* .tile_content is position:relative and later in the DOM, so it paints over
     this slot (its left padding-gutter covers it). Lift it above the content so
     the click lands on the toggle / badge, not the text. */
  z-index: 2;
  top: calc(
    var(--editor-tile-padding-block, var(--editor-tile-padding))
    + var(--editor-tile-content-padding)
    + (var(--editor-tile-line-height) * 0.5em)
    - (var(--editor-tile-todo-toggle-size) / 2)
  );
  left: var(--editor-tile-padding);
  width: var(--editor-tile-todo-toggle-size);
  height: var(--editor-tile-todo-toggle-size);
  box-sizing: border-box;
}

.tile[data-type="todo"][data-todo-complete="true"] > .tile_content {
  color: var(--tile-local-ink, var(--color-text-muted));
  text-decoration-line: var(--tile-text_underline,) line-through;
  text-decoration-thickness: 1.5px;
}

/* H-level badge text. The slot geometry (position/size) is shared with the
   todo toggle above; this is just the "H1"/"H2"/"H3" glyph centered in it. Sized
   to fit two chars in the --icon-md (14px) square. Clicking it cycles the level
   (view_dom.js), so it takes pointer events. */
.tile_header_indicator {
  /* Renders as a real <button> (D3) — reset the native chrome so it reads
     identically to the old span. */
  appearance: none;
  background: var(--tile-reading-fill, transparent);
  border: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  user-select: none;
  font-family: var(--font-sans);
  font-size: 9.5px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--tile-local-ink, var(--color-text-muted));
  transition: color 100ms ease;
}

.tile_header_indicator:hover {
  color: var(--tile-local-ink, var(--color-text-strong));
}

/* The Link identifier keeps URL editing separate from the inline destination
   label. Its four letters need a wider gutter than the heading/todo square. */
.tile_external_link_edit_button {
  appearance: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--editor-tile-link-label-width);
  font-family: var(--font-sans);
  font-size: var(--text-size-label);
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  padding: 1px;
  border: 0;
  border-radius: 4px;
  background: var(--tile-reading-fill, var(--color-surface-page));
  color: var(--tile-local-ink, var(--color-info));
  cursor: pointer;
}

.tile_external_link_edit_button:hover {
  text-decoration: underline;
}

.tile_external_link_edit_button:focus-visible {
  outline: 2px solid var(--tile-fill-ink, var(--color-info));
  outline-offset: 1px;
}

/* Make room for the badge, exactly as the todo content gutter does. */
.tile[data-type="header"] > .tile_content {
  padding-left: calc(
    var(--editor-tile-content-padding)
    + var(--editor-tile-todo-toggle-size)
    + var(--editor-tile-todo-toggle-gap)
  );
}

.tile_children_block {
  margin-top: var(--editor-child-grid-gap-top);
  display: flex;
  align-items: flex-start;
  gap: 2px;
}

/* Horizontal layout (style.layout='horizontal'): the children grid sits BESIDE
   the tile content instead of below. The td must stay a table-cell (the grid
   needs it), so the two flow inline — content as inline-block, the children
   block as inline-flex to keep its own bracket+table row. They're appended with
   no whitespace node between them, so there's no inline gap; the margin-left is
   the intended beside-gap. */
.tile[data-layout="horizontal"] > .tile_content:not(.hoist-pseudo-content),
.editor-view-root[data-default-layout="horizontal"] .tile:not([data-layout]) > .tile_content:not(.hoist-pseudo-content) {
  display: inline-block;
  vertical-align: top;
}

.tile[data-layout="horizontal"] > .tile_children_block,
.editor-view-root[data-default-layout="horizontal"] .tile:not([data-layout]) > .tile_children_block {
  display: inline-flex;
  vertical-align: top;
  margin-top: 0;
  margin-left: var(--editor-child-grid-gap-top);
}

.tile-children-table {
  position: relative;
}

/* Grid style comes from the document root unless a tile overrides its own
   children table. Each deeper subgrid independently uses the root default. */
.tile[data-grid-style="grid"] > .tile_children_block > .tile-children-table,
.editor-view-root[data-default-grid-style="grid"] .tile:not([data-grid-style]) > .tile_children_block > .tile-children-table,
.editor-view-root[data-default-grid-style="grid"] > .editor-view-grid {
  border-collapse: collapse;
  border-spacing: 0;
}

/* In collapsed mode adjacent cells SHARE one border, so a per-cell border_color
   could only win some edges. Keep the shared gridline at the default color and
   render the cell's chosen color via the (layout-neutral) outline instead, so
   each tinted cell reads exactly (decision: color-via-outline in grid mode). The
   outline defaults to 1px here so a color shows without also setting thickness.
   :where() keeps this rule's specificity low (just `td.tile`) so the
   current-tile rule above still wins and recolors the current cell to the
   selection color. */
:where(
  .tile[data-grid-style="grid"] > .tile_children_block > .tile-children-table > tbody > tr,
  .editor-view-root[data-default-grid-style="grid"] .tile:not([data-grid-style]) > .tile_children_block > .tile-children-table > tbody > tr,
  .editor-view-root[data-default-grid-style="grid"] > .editor-view-grid > tbody > tr,
  .grid-window-table[data-grid-style="grid"] > tbody > tr
) > td.tile {
  border-color: var(--editor-tile-border-color);
  border-radius: 0;
  --tile-outline-color: var(--tile-border_color, transparent);
  --tile-outline-w: var(--tile-outline_width, 1px);
}

/* Explicit Border None hides the actual edge too, without changing geometry.
   Missing width still uses the grid default. Mode/range outlines remain usable. */
.tile[data-border-none="true"] {
  border-color: transparent;
}

.tile_fold_bracket {
  flex: 0 0 auto;
  /* Top-aligned with the children grid beside it. The caret is centered inside
     this box, so the box height sets how far down the caret sits (expanded) AND
     keeps the caret on the box center — which the folded rule centers against
     the "N × M hidden" chip. */
  align-self: flex-start;
  /* Match the todo toggle's column width so the caret centers on the same
     vertical axis as a todo checkbox when a tile is both folded and a to-do. */
  width: var(--editor-tile-todo-toggle-size);
  height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  appearance: none;
  background: var(--tile-reading-fill, transparent);
  padding: 0;
  border: 0;
  cursor: pointer;
}

/* Disclosure caret (same glyph as the toolbar Fold button): points down when
   expanded, rotates to point right when folded. */
.tile_fold_bracket > svg {
  width: 12px;
  height: 12px;
  color: var(--tile-local-ink, var(--color-text-muted));
  transform: rotate(90deg);
  transition: transform 100ms ease;
}

.tile_fold_bracket:hover > svg {
  color: var(--tile-local-ink, var(--color-text-strong));
}

.tile[data-folded="true"] > .tile_children_block > .tile_fold_bracket > svg {
  transform: rotate(0deg);
}

.tile[data-type="root"] {
  border: none;
  width: max-content;
  display: block;
  padding: 0;
  background: transparent;
  box-shadow: none;
}

.tile[data-type="root"] > .tile_content {
  font-variant: small-caps;
  font-weight: bold;
  letter-spacing: 0.05em;
  font-size: 1.2rem;
  display: none;
}

.tile[data-type="root"] > .tile_children_block {
  margin: 0;
  padding: 0;
}

.tile[data-type="root"] > .tile_children_block > .tile_fold_bracket {
  display: none;
}

.tile[data-type="header"] > .tile_content {
  font-weight: var(--tile-font_weight, 700);
  letter-spacing: -0.01em;
}

.tile[data-type="header"][data-header-level="1"] > .tile_content {
  font-size: 1.7rem;
  line-height: 1.08;
}

.tile[data-type="header"][data-header-level="2"] > .tile_content {
  font-size: 1.35rem;
  line-height: 1.14;
}

.tile[data-type="header"][data-header-level="3"] > .tile_content {
  font-size: 1.12rem;
  line-height: 1.18;
}

/* Empty text / header tiles with children act as pure containers when idle:
   content folds out of layout, children sit flush against the tile edge.
   `data-collapsible-empty` is set in JS (sync_collapsible_empty_marker) so
   the CSS only needs to gate on idle state — not current, not selected.
   Keyed on the tile's OWN `> .tile_content:not(:focus)` (then a sibling
   combinator reaches the children block / fold bracket), NOT :not(:focus-within)
   — the latter also matches when a nested CHILD is focused, which wrongly
   expanded an empty parent while editing a child. We also avoid the earlier
   :not(:has(> .tile_content:focus)) form: that live :has()+:focus probe forces a
   style recalc of every collapsible tile on each focus move. Keying directly on
   the content's own `:focus` lets the browser scope invalidation to the focused
   element + its combinator siblings — same own-content semantics, cheaper
   recalc, and (unlike a data-* attribute) no DOM trace, so a patched tile still
   equals a fresh render. The `> .tile_content` and `~` stay inside this tile;
   nested tiles live in a sibling block, so nothing leaks downward.
   Childless empty tiles never get the marker, so they keep their content
   block and tile shape. */
.tile[data-collapsible-empty="true"]:not([data-current-tile="true"]):not([data-selected="true"]) > .tile_content:not(:focus) {
  display: none;
}
/* The header level badge collapses with the (empty) content. Kept as its own
   rule so the cheap own-content :focus form above (perf branch) stays intact;
   :has is scoped to just the badge element. */
.tile[data-collapsible-empty="true"]:not([data-current-tile="true"]):not([data-selected="true"]):not(:has(> .tile_content:focus)) > .tile_header_indicator {
  display: none;
}
.tile[data-collapsible-empty="true"]:not([data-current-tile="true"]):not([data-selected="true"]) > .tile_content:not(:focus) ~ .tile_children_block {
  margin-top: 0;
}

/* A delete-all repair leaves one usable blank text tile. Give that otherwise
   unlabeled recovery point one quiet hint; it only belongs to a truly empty
   root, so creating child content also removes it from the parent tile. */
.editor-view-grid > tbody > tr:only-child > .tile[data-type="text"][data-has-children="false"]:only-child > .tile_content:empty::before,
.editor-view-grid > .grid-window-row > .grid-window-table[data-row-count="1"][data-column-count="1"] > tbody > tr > .tile[data-type="text"][data-has-children="false"] > .tile_content:empty::before {
  color: var(--tile-local-ink, var(--color-text-muted));
  content: 'Type to start';
  pointer-events: none;
}


.tile_diagram_preview {
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  overflow: auto;
  scrollbar-gutter: stable;
}

/* Diagram engines arrive asynchronously and expose their aspect ratio only
   after rendering. A stable 4:3 viewport keeps the footer and its Edit target
   fixed from first paint; unusually tall/wide diagrams fit inside the viewport
   instead of moving later grid rows during pointer activation. */
.tile_media_frame:has(.tile_diagram_preview) {
  aspect-ratio: 4 / 3;
}

/* Mermaid/Graphviz/Pikchr SVGs arrive with light-theme colors baked into their
   markup. Invert the rendered bitmap in dark mode (and rotate hues back) so a
   pale diagram never becomes a flashlight on the dark workspace. */
[data-theme="dark"] .tile_diagram_preview > svg {
  filter: invert(0.88) hue-rotate(180deg);
}

/* Fill the card width (not just cap it) so the diagram scales with the resize
   both ways — otherwise widening the card leaves the diagram at its natural size
   with empty space beside it. */
.tile_diagram_preview svg {
  width: 100%;
  height: 100%;
}


/* Inline marks (see inline_marks.js): styled runs + always-visible dimmed
   delimiters. Strike+underline needs the combined rule because
   text-decoration does not accumulate across classes. */
.tile_mark_bold {
  font-weight: 700;
}

.tile_mark_italic {
  font-style: italic;
}

.tile_mark_strike {
  text-decoration: line-through;
}

.tile_mark_underline {
  text-decoration: underline;
}

.tile_mark_strike.tile_mark_underline {
  text-decoration: underline line-through;
}

.tile_mark_highlight {
  background: var(--color-highlight);
  color: var(--color-text-strong);
}

/* Highlight is a painted surface; its editable delimiters need full ink. */
.tile_mark_highlight.tile_mark_delim,
.tile_mark_code.tile_mark_delim {
  color: var(--color-text-strong);
}

.tile_mark_code {
  font-family: var(--font-mono);
  font-size: 0.9em;
  background: var(--color-surface-sunken);
  color: var(--color-text-strong);
  border-radius: 4px;
  padding: 0.05em 0.3em;
}

.tile_mark_link:not(.tile_mark_highlight, .tile_mark_code) {
  color: var(--tile-local-ink, var(--color-accent));
}

.tile_mark_link {
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.tile_external_link_label {
  color: var(--tile-local-ink, var(--color-info));
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.tile[data-external-link-state="invalid"] > .tile_content > .tile_external_link_label {
  color: var(--tile-local-ink, var(--color-error));
  text-decoration-style: wavy;
}

.tile_external_link_label:empty::before {
  content: attr(data-placeholder);
  color: var(--tile-local-ink, var(--color-text-muted));
  text-decoration: none;
}

/* Delimiters collapse at rest (display:none — no reserved width, no gaps) and
   reveal (dimmed) only on the editor's CURRENT tile (its "virtual focus").
   Keyed on [data-current-tile], NOT .tile_content:focus — DOM focus leaves the
   tile when you click a toolbar action (a mark/style button), which made the
   delimiters flicker. The current-tile marker persists across those clicks. The
   `> .tile_content` scope keeps it to the current tile's OWN delimiters: nested
   tiles' content lives in a sibling .tile_children_block, so this never leaks
   downward. textContent is unaffected, so cursor math and the inline-marks
   invariant hold. */
.tile_mark_delim {
  color: var(--tile-local-ink, var(--color-text-muted));
  display: none;
}

.tile[data-current-tile="true"] > .tile_content .tile_mark_delim {
  display: inline;
}

/* Links keep their underline on selection washes; accent-on-accent falls below
   the text contrast floor in both themes. Only this tile's own text is affected. */
.tile:is([data-current-tile="true"], [data-selected="true"]) > .tile_content .tile_mark_link:not(.tile_mark_highlight, .tile_mark_code),
.tile:is([data-current-tile="true"], [data-selected="true"]) > .tile_content .tile_external_link_label {
  color: var(--tile-local-ink, var(--color-text-strong));
}
