/* ============================================================
   Superus — component styles
   Only semantic tokens from tokens.css are used here.
   ============================================================ */
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body { font-family: var(--font-ui); font-size: var(--fs-md); letter-spacing: var(--ls-13); line-height: var(--lh-base); color: var(--text-primary); background: var(--surface-sunken); overflow: hidden; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; font-feature-settings: "cv11", "ss01"; }
/* UA 样式对表单控件写死 letter-spacing: normal，`font:` 简写又会重置
   font-optical-sizing —— 不显式继承的话，整个交互层都会掉出 §4.2 的字距阶梯。 */
button { font: inherit; letter-spacing: inherit; font-optical-sizing: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
button:disabled { cursor: default; }
input, textarea, select { font: inherit; letter-spacing: inherit; font-optical-sizing: inherit; }
/* One keycap. A fixed box rather than padding around text: an inline <kbd> took its
   height from the font's ascent (19px) and one in a menu row from line-height (17px),
   so the same key drew at three heights across menus, the palette and the shortcuts. */
kbd { display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; height: 18px; min-width: 18px; white-space: nowrap; font: var(--fw-medium) var(--fs-xs)/1 var(--font-ui); letter-spacing: var(--ls-11); color: var(--text-tertiary); background: var(--fill-subtle); border: 1px solid var(--border-default); border-radius: var(--r-xs); padding: 0 5px; }
svg { width: var(--icon-lg); height: var(--icon-lg); display: block; flex: none; }
[hidden] { display: none !important; }
:focus-visible { outline: 2px solid var(--accent-line); outline-offset: 2px; }
/* A match highlight has to read on the palette's own selected row (an accent tint)
   and on both themes — it was 16% white, which is nothing at all on a white palette. */
mark { background: color-mix(in oklch, var(--warning) 36%, transparent); color: inherit; border-radius: var(--r-xs); }
.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; }
.grow { flex: 1; }

/* ---------- type utilities ---------- */
.t-eyebrow { font-size: var(--fs-2xs); font-weight: var(--fw-bold); letter-spacing: var(--ls-caps); text-transform: uppercase; }
.t-label { font-size: var(--fs-xs); font-weight: var(--fw-semibold); letter-spacing: var(--ls-caps); text-transform: uppercase; color: var(--text-secondary); }
.t-meta { font-size: var(--fs-xs); letter-spacing: var(--ls-11); color: var(--text-tertiary); }

/* ---------- motion primitives ---------- */
@keyframes fade-in { from { opacity: 0; } }
@keyframes rise-in { from { opacity: 0; translate: 0 var(--motion-offset); } }
@keyframes drop-in { from { opacity: 0; translate: 0 calc(-1 * var(--motion-offset)); } }
@keyframes scale-in { from { opacity: 0; scale: var(--motion-scale); } }
/* a dialog or the palette: rises a few pixels as it scales up, settling long */
@keyframes sheet-in { from { opacity: 0; translate: 0 var(--motion-offset); scale: var(--motion-scale); } }
@keyframes card-pop { from { opacity: 0; scale: 0.94; } }
@keyframes card-right { from { opacity: 0; translate: 56px 0; scale: 0.98; } }
@keyframes card-up { from { opacity: 0; translate: 0 18px; scale: 0.98; } }
@keyframes fade-out { to { opacity: 0; } }
@keyframes sink-out { to { opacity: 0; translate: 0 var(--motion-offset); } }
@keyframes scale-out { to { opacity: 0; scale: var(--motion-scale); } }
@keyframes draw-in { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
@keyframes tip-in { from { opacity: 0; translate: 0 var(--motion-offset); } }
@keyframes toast-out { to { opacity: 0; translate: 0 var(--motion-offset); } }
@keyframes flash-ring { 0% { box-shadow: 0 0 0 0 var(--accent-ring); } 100% { box-shadow: 0 0 0 18px transparent; } }
@keyframes shimmer { to { background-position: -200% 0; } }
@keyframes march { to { stroke-dashoffset: -12; } }
@keyframes pulse { 50% { opacity: 0.4; } }
@keyframes shake { 25% { transform: translateX(-4px); } 75% { transform: translateX(4px); } }

/* ---------- shared control primitives ---------- */
.icon-btn { width: var(--ctl-md); height: var(--ctl-md); border-radius: var(--r-control); display: grid; place-items: center; color: var(--text-secondary); transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out); }
.icon-btn:hover { background: var(--state-hover); color: var(--text-primary); }
.icon-btn:active { background: var(--state-active); }
.icon-btn.on { color: var(--text-primary); background: var(--state-selected); }
.icon-btn.small { width: var(--ctl-sm); height: var(--ctl-sm); } .icon-btn.small svg { width: var(--icon-md); height: var(--icon-md); }
.icon-btn:disabled { opacity: var(--state-disabled-opacity); }
.icon-btn:disabled:hover { background: none; color: var(--text-secondary); }
.text-btn { height: var(--ctl-md); padding: 0 var(--sp-3); border-radius: var(--r-control); color: var(--text-primary); font-weight: var(--fw-medium); transition: background var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out), filter var(--dur-fast); }
.text-btn:not(:disabled):hover { background: var(--state-hover); }
.text-btn:not(:disabled):active { background: var(--state-active); }
.text-btn.primary { background: var(--accent); color: var(--text-on-accent); }
.text-btn.primary:not(:disabled):hover { background: var(--accent-hover); }
.text-btn.primary:not(:disabled):active { background: var(--accent-active); }
.text-btn.ghost { background: var(--fill-subtle); }
.text-btn.ghost:not(:disabled):hover { background: var(--fill-strong); }
.text-btn.danger { background: var(--action-danger-bg); color: var(--action-danger-fg); }
.text-btn.danger:not(:disabled):hover { background: var(--action-danger-bg-hover); }
.text-btn.danger:not(:disabled):active { background: var(--action-danger-bg-active); }
.link-btn { color: var(--text-secondary); font-weight: var(--fw-medium); }
.link-btn:hover { color: var(--text-primary); }
.link-btn:hover { text-decoration: underline; }
/* Third-party marks keep their original colours and proportions. A page-white
   backing protects transparent dark marks in every theme; inset prevents clipping. */
:is(.fav, .pv-fav, .lp-fav, .chip img, .pf-site img, .site img, .recent img, .g-res-url img, .g-snip-src img) {
  box-sizing: border-box; object-fit: contain; object-position: center;
  padding: 1px; border-radius: var(--r-xs); flex: none;
  background: var(--surface-page); -webkit-user-drag: none;
}
.site img { padding: var(--sp-1); }
.url-preview .fav { padding: 2px; }

.app { display: flex; flex-direction: row; height: 100vh; height: 100dvh; }
.main-col { flex: 1; min-width: 0; display: flex; flex-direction: column; }

/* ---------- top bar ---------- */
.topbar { height: 44px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 0 var(--sp-3); background: var(--surface-chrome); border-bottom: 1px solid var(--border-default); position: relative; z-index: var(--z-chrome); }
.topbar-left, .topbar-right { display: flex; align-items: center; gap: var(--sp-1); min-width: 0; }
.topbar-left > .icon-btn, .topbar-right > button { flex-shrink: 0; }
.topbar-right { justify-content: flex-end; }
.traffic { display: flex; gap: 7px; padding: 0 6px 0 2px; }
.tl { width: 12px; height: 12px; border-radius: 50%; background: var(--text-disabled); }
.tl.red { background: var(--traffic-red); } .tl.yellow { background: var(--traffic-yellow); } .tl.green { background: var(--traffic-green); }
.divider { width: 1px; height: 18px; background: var(--border-default); margin: 0 6px; }
.board-name { display: flex; align-items: center; gap: var(--sp-1); min-width: 0; height: var(--ctl-md); padding: 0 var(--sp-2); border-radius: var(--r-sm); font-weight: var(--fw-semibold); letter-spacing: var(--ls-13); transition: background var(--dur-fast) var(--ease-out); }
.board-name #board-name-text { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.board-name:hover { background: var(--state-hover); }
/* The disclosure chevron is an answer to a question nobody is asking until they
   point at the name, so it waits until they do. It fades rather than being
   removed from the box, so the title never shifts under the pointer — and it
   stays up while the menu it opened is open. */
.board-name .chev { opacity: 0; transition: opacity var(--dur-fast) var(--ease-out); }
.board-name:hover .chev, .board-name:focus-visible .chev, .board-name.open .chev { opacity: 1; }
.board-name .chev svg { width: var(--icon-sm); height: var(--icon-sm); color: var(--text-tertiary); }
/* Sits last in the top bar's left group, so appearing and disappearing moves
   nothing beside it. */
.save-state { margin-left: var(--sp-1); flex: none; white-space: nowrap; font-size: var(--fs-xs); letter-spacing: var(--ls-11); color: var(--text-tertiary); transition: color var(--dur-base), opacity var(--dur-base) var(--ease-in); animation: fade-in var(--dur-base) var(--ease-out); }
.save-state.busy { color: var(--text-secondary); }
.save-state.leaving { opacity: 0; }
/* The only state worth a permanent place in the chrome is the one that means
   the board is not where the user thinks it is. It gets a chip, not a word. */
.save-state.failed { color: var(--danger-text); background: var(--danger-soft); border: 1px solid var(--danger-border); border-radius: var(--r-sm); padding: 2px var(--sp-2); font-weight: var(--fw-medium); animation: shake var(--dur-feedback) var(--ease-out); }
/* While it fails the chip is a button (it says what to do); the global :focus-visible draws its ring. */
.save-state.failed[role=button] { cursor: pointer; }
.save-state.failed[role=button]:hover { background: color-mix(in oklch, var(--danger-soft), var(--danger-text) 10%); }
.avatars { display: flex; margin-right: 6px; }
.avatar { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; font-size: var(--fs-xs); letter-spacing: var(--ls-11); font-weight: var(--fw-bold); color: var(--text-on-accent); background: var(--avatar-gradient); border: 2px solid var(--surface-chrome); }

/* ---------- layout ---------- */
.main { flex: 1; display: flex; min-height: 0; position: relative; }

/* ---------- outline ---------- */
/* Mirror Side Peek inside .main. Neither drawer changes the viewport or chrome.
   Visibility is synchronous so native Mac pages can share the same clipping edge. */
.outline { position: absolute; left: 0; top: var(--sp-3); bottom: var(--sp-3); width: 260px; max-width: calc(100% - 24px); z-index: var(--z-toolbar); background: var(--surface-raised); border: 1px solid var(--overlay-border); border-left: 0; border-radius: 0 var(--r-lg) var(--r-lg) 0; box-shadow: var(--shadow-lg); display: flex; flex-direction: column; overflow: hidden; }
.outline.collapsed { visibility: hidden; pointer-events: none; }
.outline-head { height: 48px; flex: none; display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); padding: 0 var(--sp-2) 0 var(--sp-4); border-bottom: 1px solid var(--border-subtle); color: var(--text-secondary); font-weight: var(--fw-semibold); font-size: var(--fs-md); letter-spacing: var(--ls-13); min-width: 0; }
/* The sidebar opens and closes from the bottom bar's left (#btn-outline), which stays
   in view beside it: one way to close it. Only the phone-width drawer (below) carries
   its own close button. */
#btn-outline-close { display: none; }
.outline-head-actions { display: flex; gap: 2px; }
/* The sidebar's own scrollbar is the canvas overlay scrollbar in a track: same
   tokens, same alpha, same radius. A native bar next to the ones on the board
   would be the only piece of unstyled chrome in the app. */
/* Keep the top gap inside the scrollport so sticky section headers cover rows. */
.outline-list { flex: 1; min-height: 0; overflow: auto; overflow-x: hidden; padding: var(--sp-2) var(--sp-2) var(--sp-1); min-width: 0; scrollbar-width: thin; scrollbar-color: var(--sb-thumb) transparent; }
.outline-list::-webkit-scrollbar { width: 11px; }
.outline-list::-webkit-scrollbar-track { background: transparent; }
.outline-list::-webkit-scrollbar-thumb { background: var(--sb-thumb); background-clip: padding-box; border: 3px solid transparent; border-radius: var(--r-full); }
.outline-list::-webkit-scrollbar-thumb:hover { background: var(--sb-thumb-hover); background-clip: padding-box; }

/* One height for every row, whatever it holds. The action button used to be a
   grid column, so a row with one stood 34px tall and a row without stood 31 —
   the list never sat on a grid, and the eye reads that as slop long before it
   can name it. The button overlays now and the row is 28px, full stop.

   Depth is a grid SPACER COLUMN inside a row that always spans the full panel,
   which is GitHub's technique (per Ahmad Shadeed's survey of how the design
   systems do this) and the reason hover and selection line up at every level.
   Indenting with margin — what this used to do — moved the row box itself, so a
   selected child's tint started 18px in while its parent's started at 0.
   Photoshop's hardcoded per-level padding and Carbon's negative margins are the
   other two ways to get this wrong; one does not scale past its table of
   values, the other breaks the click target.

   12px a level, not 18: Primer's guidance is to keep a tree horizontally
   compact so it can afford both depth and long labels, and 260px does not have
   room to be generous about either. */
.ol-item {
  --indent: 12px;
  --gutter: var(--sp-2);
  position: relative; height: var(--row-sm); display: grid;
  grid-template-columns: 18px 1fr auto;
  align-items: center; column-gap: var(--sp-2); border-radius: var(--r-sm);
  /* The indent is padding on a row that always spans the full panel. A grid
     spacer column would do the same job, but its column-gap still applies at
     level 0, which pushes every root row 8px right of where the guides expect
     it — one variable in one place is easier to keep honest. */
  padding: 0 var(--gutter) 0 calc(var(--gutter) + var(--level, 0) * var(--indent));
  color: var(--text-primary); cursor: default; user-select: none; min-width: 0;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
  /* One hairline per ancestor: a 1px stripe repeating on the indent, clipped to
     the width of the indent itself, so a row at level N draws exactly N of them.
     It scales to any depth without a rule per level, and because the stripe
     period and the indent are the same number, every guide lands dead on the
     centre of the icon column of the level it belongs to — a guide always points
     at the row that owns it. */
  background-image: repeating-linear-gradient(to right, var(--tree-guide) 0 1px, transparent 1px var(--indent));
  background-size: calc(var(--level, 0) * var(--indent)) 100%;
  background-position: calc(var(--gutter) + 9px) 0;
  background-repeat: no-repeat;
}
.ol-item .fav, .ol-item .ti { justify-self: center; }
.ol-item:hover, .ol-item.canvas-hover { background-color: var(--state-hover); }
/* Selection is a translucent layer, like every other state in the system, not a
   solid accent slab. Two reasons it cannot be solid here: the canvas card
   already spends the one solid accent this view is allowed on its selection
   ring, and a marquee over ten cards would turn the whole sidebar into a block
   of indigo. The tint carries the state; the accent icon and the medium weight
   make sure it still reads as *the* selected row and not merely a warm one. */
.ol-item.on { background-color: var(--state-selected); }
.ol-item.on .ol-title { font-weight: var(--fw-medium); }
.ol-item.on .ti svg { color: var(--accent-text); }
.ol-item .fav { width: var(--icon-md); height: var(--icon-md); border-radius: var(--r-xs); }
.ol-item .ti svg { width: var(--icon-md); height: var(--icon-md); color: var(--text-secondary); }
/* A frame, a map or a branch folds from its icon, as Notion's sidebar folds a page: under the
   pointer (or the keyboard's focus) the icon becomes a chevron, and a folded row keeps it,
   turned, so what is hidden is never a surprise. The two share the icon's cell, so nothing
   in the row moves. */
.ol-item > :is(.ti, .fav, .ol-fold) { grid-area: 1 / 1; transition: opacity var(--dur-fast); }
.ol-item > .ol-title { grid-column: 2; }
.ol-item > .ol-count { grid-column: 3; }
.ol-fold { justify-self: center; width: 18px; height: 18px; border-radius: var(--r-xs); display: grid; place-items: center; color: var(--text-secondary); opacity: 0; }
.ol-fold svg { width: var(--icon-sm); height: var(--icon-sm); transition: transform var(--dur-base) var(--ease-out); }
.ol-item.folded .ol-fold svg { transform: rotate(-90deg); }
.ol-item.can-fold:is(:hover, :focus-visible, .folded) > .ol-fold { opacity: 1; }
.ol-item.can-fold:is(:hover, :focus-visible, .folded) > :is(.ti, .fav) { opacity: 0; }
.ol-fold:hover { background: var(--state-hover); color: var(--text-primary); }
/* How many cards a frame holds, or how many items are inside a sub-canvas —
   the one piece of metadata that answers "what is in there" for a row whose
   contents are not on screen. Quiet: it is a footnote to the name. */
.ol-count { font-size: var(--fs-xs); letter-spacing: var(--ls-11); color: var(--text-tertiary); font-variant-numeric: tabular-nums; padding-left: var(--sp-2); }
.ol-item.on .ol-count { color: var(--text-secondary); }
/* A frame is a section, so it is set as one — and it sticks, because on a board
   with several frames the thing you most need while scrolling a long list is
   which region you are currently inside. The list's 8px gutters stay empty at
   every scroll position, so an opaque row inset by the same 8px covers
   everything that could pass behind it. */
/* Nested frames all stick, so they have to stack rather than land on top of one
   another: each parks one row-height lower than its parent. Capped at three deep
   — past that the stack would eat the list it is meant to label. */
.ol-item.t-frame {
  position: sticky; top: calc(min(var(--level, 0), 3) * 28px);
  /* Shallower headers park higher AND paint above, so a parent is never pushed
     behind the child it contains as the two meet. */
  z-index: calc(4 - min(var(--level, 0), 3));
  background-color: var(--surface-chrome);
}
.ol-item.t-frame:not(:first-child) { margin-top: var(--sp-2); }
/* The states are alpha layers, so on a sticky header they go over the chrome, not
   instead of it: alone they let the rows scrolling under the header show through. */
.ol-item.t-frame:hover { background: linear-gradient(var(--state-hover), var(--state-hover)), var(--surface-chrome); }
.ol-item.t-frame.on { background: linear-gradient(var(--state-selected), var(--state-selected)), var(--surface-chrome); }
/* A frame's name is the person's own words, so it is set as they wrote it: semibold and
   secondary marks it as a section, and the whole name fits. In caps (a system label's
   setting) "Where the idea comes from" cut off at "WHERE THE IDEA COMES …". */
.ol-item.t-frame .ol-title { font-size: var(--fs-sm); font-weight: var(--fw-semibold); letter-spacing: var(--ls-12); color: var(--text-secondary); }
.ol-item.t-frame .ti svg { color: var(--text-tertiary); }
.ol-item.t-frame.on .ti svg { color: var(--accent-text); }
.ol-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The title owns the full width of the row. Reserving a column for a button
   that is invisible 95% of the time cost every source row 32px of name — which
   is why "Visit Canada - Canada.ca" had to truncate at 143px inside a 260px
   panel. On hover the title is masked out from under the button instead, so
   nothing moves and no background has to be matched. */
.ol-item:hover .ol-title, .ol-item:focus-within .ol-title {
  mask-image: linear-gradient(90deg, #000 calc(100% - 20px), transparent calc(100% - 4px));
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 20px), transparent calc(100% - 4px));
}
.ol-item.t-frame:hover .ol-title { mask-image: none; -webkit-mask-image: none; }
.ol-act {
  position: absolute; right: 2px; top: 50%; translate: 0 -50%;
  width: 20px; height: 20px; border-radius: var(--r-xs); display: grid; place-items: center;
  color: var(--text-tertiary); opacity: 0; transition: opacity var(--dur-fast), background var(--dur-fast), color var(--dur-fast);
}
.ol-act svg { width: var(--icon-sm); height: var(--icon-sm); }
.ol-item:hover .ol-act, .ol-item:focus-within .ol-act { opacity: 1; }
.ol-act:hover { background: var(--state-hover); color: var(--text-primary); }
.ol-act:active { background: var(--state-active); }
/* A pressed toggle has to stay visible when the pointer leaves, or the only
   sign that the panel belongs to this row vanishes the moment you move away —
   and then the button that closes Side Peek is one you cannot see. */
.ol-item .ol-act.on { opacity: 1; background: var(--state-selected); color: var(--accent-text); }
.card.peek-source .card-head-actions { opacity: 1; }
.chb.on, .card-toolbar button.on { background: var(--state-selected); color: var(--accent-text); }
.ol-empty { padding: var(--sp-6) var(--sp-3); color: var(--text-tertiary); line-height: var(--lh-relaxed); text-align: center; }
.outline-foot { height: 44px; flex: none; display: flex; align-items: center; padding: 0 var(--sp-4); border-top: 1px solid var(--border-default); color: var(--text-tertiary); font-size: var(--fs-xs); letter-spacing: var(--ls-11); white-space: nowrap; min-width: 0; font-variant-numeric: tabular-nums; }
#outline-count { overflow: hidden; text-overflow: ellipsis; }

/* ---------- viewport ---------- */
.viewport { flex: 1; position: relative; overflow: hidden; background-color: var(--surface-sunken); cursor: default; user-select: none;
  box-shadow: none; isolation: isolate; }
/* Grid on its own layer, beneath .world: two dot levels an octave apart,
   crossfaded by app.js. Each layer's position is pulled back half a tile so
   both cascades share a phase and the coarse dots land on every second fine
   dot instead of interleaving with them.
   The dots are painted once on a tile one coarse period larger than the view on
   every side, and a pan moves that tile with a transform, modulo the period —
   the pattern repeats, so the picture is identical. Moving background-position
   instead repainted and re-rasterised the whole viewport's texture every pan
   frame (~20ms of raster on a 2x screen); a transform is compositor work only. */
.canvas-grid {
  position: absolute; inset: 0; pointer-events: none; contain: strict; overflow: hidden;
  mask-image: linear-gradient(to bottom, transparent, #000 var(--canvas-edge-fade)),
              linear-gradient(to right, transparent, #000 var(--canvas-edge-fade));
  mask-composite: intersect;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 var(--canvas-edge-fade)),
                      linear-gradient(to right, transparent, #000 var(--canvas-edge-fade));
  -webkit-mask-composite: source-in;
  transition: opacity var(--dur-slow) var(--ease-out);
}
.grid-tile {
  position: absolute; left: 0; top: 0;
  width: calc(100% + var(--grid-sp, var(--grid-sb, 48px)) * 2); height: calc(100% + var(--grid-sp, var(--grid-sb, 48px)) * 2);
  background-image:
    radial-gradient(circle at center, color-mix(in oklch, var(--canvas-grid-dot) calc(var(--grid-aa, 1) * 100%), transparent) 0 var(--canvas-dot-r), transparent var(--canvas-dot-feather)),
    radial-gradient(circle at center, color-mix(in oklch, var(--canvas-grid-dot) calc(var(--grid-ab, 0) * 100%), transparent) 0 var(--canvas-dot-r), transparent var(--canvas-dot-feather));
  background-size: var(--grid-sa, 24px) var(--grid-sa, 24px), var(--grid-sb, 48px) var(--grid-sb, 48px);
  background-position: calc(var(--grid-sa, 24px) / -2) calc(var(--grid-sa, 24px) / -2), calc(var(--grid-sb, 48px) / -2) calc(var(--grid-sb, 48px) / -2);
  will-change: transform; /* moved by app.js with style.transform — Chrome's direct composited-transform path */
}
.app.present .canvas-grid { opacity: 0; }
/* Snap to grid on (app.js showSnapGrid): the dots cards land on read a little stronger. */
.canvas-grid.snap .grid-tile { --canvas-grid-dot: light-dark(oklch(20% calc(0.02 * var(--chroma-n)) var(--hue-n) / 0.19), oklch(100% 0 0 / 0.1)); }
/* The board's background (app.js BOARD_BACKGROUNDS, ··· ▸ Background): the same cascade on
   the same 24-point snap grid, drawn as squared paper, as ruled lines, or not at all.
   Rules are repeating gradients across the whole tile, not tiled images: a tile of
   fractional size is rasterised once and resampled, which softened a 1px rule by a
   different amount on every row. A gradient is evaluated per pixel, so each rule is one
   crisp line of whole device pixels (app.js keeps the tile itself on whole pixels too).
   Squared paper repeats every eight fine lines: the eighth is a heavier line, the fourth
   fades from heavy to fine and the odd ones fade out as the next octave takes over
   (--grid-aa), so a heavier line falls every fourth square at any zoom. */
.canvas-grid[data-bg] .grid-tile { background-size: auto; background-position: 0 0; }
.canvas-grid[data-bg="grid"] .grid-tile {
  --gu: var(--grid-sa, 24px);
  --g1: color-mix(in oklch, var(--canvas-grid-line) calc(var(--grid-aa, 1) * 100%), transparent);
  --g4: color-mix(in oklch, var(--canvas-grid-major) calc(var(--grid-aa, 1) * 100%), var(--canvas-grid-line));
  --rules: var(--canvas-grid-major) 0 1px, transparent 1px var(--gu),
    var(--g1) var(--gu) calc(var(--gu) + 1px), transparent calc(var(--gu) + 1px) calc(var(--gu) * 2),
    var(--canvas-grid-line) calc(var(--gu) * 2) calc(var(--gu) * 2 + 1px), transparent calc(var(--gu) * 2 + 1px) calc(var(--gu) * 3),
    var(--g1) calc(var(--gu) * 3) calc(var(--gu) * 3 + 1px), transparent calc(var(--gu) * 3 + 1px) calc(var(--gu) * 4),
    var(--g4) calc(var(--gu) * 4) calc(var(--gu) * 4 + 1px), transparent calc(var(--gu) * 4 + 1px) calc(var(--gu) * 5),
    var(--g1) calc(var(--gu) * 5) calc(var(--gu) * 5 + 1px), transparent calc(var(--gu) * 5 + 1px) calc(var(--gu) * 6),
    var(--canvas-grid-line) calc(var(--gu) * 6) calc(var(--gu) * 6 + 1px), transparent calc(var(--gu) * 6 + 1px) calc(var(--gu) * 7),
    var(--g1) calc(var(--gu) * 7) calc(var(--gu) * 7 + 1px), transparent calc(var(--gu) * 7 + 1px) calc(var(--gu) * 8);
  background-image: repeating-linear-gradient(to right, var(--rules)), repeating-linear-gradient(to bottom, var(--rules));
}
/* Ruled lines: rows only, the odd ones fading out between octaves. */
.canvas-grid[data-bg="lines"] .grid-tile {
  --gu: var(--grid-sa, 24px);
  background-image: repeating-linear-gradient(to bottom,
    var(--canvas-rule-line) 0 1px, transparent 1px var(--gu),
    color-mix(in oklch, var(--canvas-rule-line) calc(var(--grid-aa, 1) * 100%), transparent) var(--gu) calc(var(--gu) + 1px), transparent calc(var(--gu) + 1px) calc(var(--gu) * 2));
}
/* Blank: no layer at all, so nothing is painted or kept in memory for it. */
.canvas-grid[data-bg="blank"] .grid-tile { display: none; }
.canvas-grid.snap .grid-tile { --canvas-grid-line: var(--canvas-grid-line-snap); --canvas-grid-major: var(--canvas-grid-major-snap); --canvas-rule-line: var(--canvas-rule-line-snap); }
/* The mirror is known to be stale: keep the board out of sight until IndexedDB answers. */
.app.store-loading .world { visibility: hidden; }
.viewport[data-tool="hand"], .viewport.space { cursor: grab; }
.viewport[data-tool="line"], .viewport[data-tool="text"], .viewport[data-tool="frame"], .viewport[data-tool="shape"] { cursor: crosshair; }

/* View access keeps navigation and sources, without offering editing tools. */
.board-viewonly :is(#btn-add,#btn-stickers,#btn-shape-kind,.fmt-bar,.rz-handle,.anchor,.link-end,.link-toolbar,.map-add,.map-fold,.tool-dock [data-dock="web"],#empty .empty-actions,#empty .empty-hint),
.board-viewonly .tool[data-tool]:not([data-tool="select"]):not([data-tool="hand"]) { display:none!important; }
.board-viewonly .card-toolbar [data-act]:not([data-act="menu"]):not([data-act="engage-open"]):not([data-act="copy-outline"]):not([data-act="copy-mermaid"]):not([data-act="src-open"]):not([data-act="clip-src"]):not([data-act="clip-open"]):not([data-act="clip-tab"]):not([data-act="clip-play"]):not([data-act="clip-view"]):not([data-act="cite"]):not([data-act="clip-cite"]):not([data-act="archive"]):not([data-act="copy-doc"]):not([data-act="clip-img-url"]):not([data-act="tbl-md"]):not([data-act="tbl-tsv"]):not([data-act="copy-png"]):not([data-act="export-png"]):not([data-act="peek"]):not([data-act="use"]):not([data-act="open"]):not([data-act="web-details"]):not([data-act="expand"]):not([data-act="open-tab"]):not([data-act="copy-url"]):not([data-act="copy"]):not([data-act="focus"]):not([data-act="fit"]):not([data-act="select-all"]) { display:none!important; }
.world { position: absolute; left: 0; top: 0; transform-origin: 0 0; will-change: transform; }
/* A still of the level being left while the camera zooms through a nested board's card
   (levelGhost / flyGhost in app.js). It sits under the live board and never takes input. */
.level-ghost { position: absolute; inset: 0; overflow: hidden; pointer-events: none; transform-origin: 0 0; will-change: transform, opacity; }
.level-ghost * { pointer-events: none !important; }
.level-ghost .card { transition: none; }
/* While the camera zooms, chrome sized from --ui snaps rather than transitions
   (app.js puts the class on the viewport for the gesture). Entry animations are untouched. */
.viewport.zooming .world, .viewport.zooming .world *, .viewport.zooming .anchors * { transition: none !important; }
/* The camera is interpolated in JS (animateTransform / camStep) rather than by a
   CSS transition: a transition moves only .world, and the dot grid, selection
   toolbar, connect anchors and minimap viewport would all snap to the destination
   on frame one and wait there for the board to catch up. */

.links, .guides { position: absolute; left: 0; top: 0; width: 1px; height: 1px; overflow: visible; pointer-events: none; }
/* Its own layer: painted inside the world's layer under every card, the svg made each
   repaint re-layerize the whole board (45ms a selection with 600 mind-map topics). */
.links { will-change: transform; }
.guides line { stroke: var(--guide); }
/* The guide pink is a light solid (L 64-70%): white measured 3.8:1 / 3.0:1 on it, deep ink 5.3 / 6.8. */
.guides .glabel rect { fill: var(--guide); }
.guides .glabel text { fill: var(--text-on-content-solid); font-family: var(--font-ui); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
.links .link { pointer-events: auto; cursor: pointer; color: var(--link-stroke); transition: color var(--dur-fast); }
.links .link.related { color: var(--link-stroke-related); }
.links .link.c-blue { color: var(--note-blue-solid); } .links .link.c-teal { color: var(--note-teal-solid); } .links .link.c-amber { color: var(--note-amber-solid); } .links .link.c-rose { color: var(--note-rose-solid); }
/* Hover is a neutral step up (this line can be picked); only a selection takes the
   accent, the same colour as a selected card's ring. Both used to be the accent, so a
   line under the pointer looked already selected. */
.links .link.c-default:not(.related, .selected):hover { color: var(--link-stroke-hover); }
.links .link.c-default.selected { color: var(--object-border-selected); }

.links .hit { fill: none; stroke: transparent; stroke-width: 20; }
/* stroke-opacity, not opacity: an opacity on every halo was an effect node per line, and
   each one split the line layer into another paint chunk (another layer to merge). */
.links .halo { fill: none; stroke: currentColor; stroke-width: 7; stroke-linecap: round; vector-effect: non-scaling-stroke; stroke-opacity: 0; transition: stroke-opacity var(--dur-fast); }
.links .link:hover .halo { stroke-opacity: 0.08; }
.links .link.selected .halo { stroke-opacity: 0.13; }
.links .stroke { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.links .stroke.dashed { stroke-dasharray: 5 5; }
.links .cap.arrow { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.links .cap.dot { fill: currentColor; stroke: none; }
.links .link.fresh { animation: fade-in var(--dur-base) var(--ease-out) both; }
.links .link.preview { color: var(--accent); pointer-events: none; }
.links .link.preview .stroke { stroke-dasharray: 4 5; opacity: 0.75; }
/* Over a card it would connect to, the proposal turns into the line it will be. */
.links .link.preview.locked .stroke { stroke-dasharray: none; opacity: 1; }
/* A selected line's end dragged off its card, not yet over another. */
.links .link.loose .stroke { stroke-dasharray: 4 5; }
.cards { position: absolute; left: 0; top: 0; }

/* connector labels (world space) */
.link-labels { position: absolute; left: 0; top: 0; }
/* A zero-size box at the label's spot (left/top, set by app.js) that centres the pill on
   it: no transform, so every pill stays in one paint chunk with the rest. */
.link-label { position: absolute; left: 0; top: 0; width: 0; height: 0; display: flex; align-items: center; justify-content: center; pointer-events: auto; }
.link-label-in { flex: none; display: inline-block; max-width: 240px; padding: 3px 8px; background: var(--canvas-bg); border: 1px solid transparent; border-radius: var(--r-sm); color: var(--text-primary); font-size: var(--fs-sm); letter-spacing: var(--ls-12); font-weight: var(--fw-medium); line-height: var(--lh-snug); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; box-shadow: none; cursor: default; outline: 0; transition: border-color var(--dur-fast), box-shadow var(--dur-fast); }
.link-label:hover .link-label-in { border-color: var(--border-strong); }
.link-label.selected .link-label-in { border-color: color-mix(in oklab, var(--accent) 40%, transparent); }
.link-label-in[contenteditable="true"] { white-space: normal; min-width: 60px; cursor: text; outline: 0; border-color: var(--field-focus-border); box-shadow: var(--ring-focus); }
.link-label-in[contenteditable="true"]:empty::before { content: "Label"; color: var(--text-tertiary); }

/* connection anchors + endpoint handles (screen-constant size) */
.anchors { position: absolute; left: 0; top: 0; z-index: 9500; pointer-events: none; }
.anchor { position: absolute; left: 0; top: 0; width: 22px; height: 22px; margin: -11px 0 0 -11px; display: grid; place-items: center; pointer-events: auto; cursor: crosshair; transform-origin: 11px 11px; animation: fade-in var(--dur-fast); }
.anchor span { width: 7px; height: 7px; border-radius: 50%; background: var(--anchor-fill); border: 1.1px solid var(--anchor-ring); opacity: 0.6;
  transition: transform var(--dur-fast) var(--ease-spring), background var(--dur-fast), border-color var(--dur-fast), opacity var(--dur-fast); }
.anchor:hover span { transform: scale(1.3); background: var(--accent); border-color: var(--accent); opacity: 1; }
.link-end { position: absolute; left: 0; top: 0; width: 8px; height: 8px; margin: -4px 0 0 -4px; border-radius: 50%; background: var(--handle-fill); border: 1.5px solid var(--handle-ring); box-shadow: 0 0 0 2px var(--handle-halo); pointer-events: auto; cursor: grab; transform-origin: 4px 4px; }
.link-end::before { content: ""; position: absolute; width: 24px; height: 24px; left: 50%; top: 50%; transform: translate(-50%, -50%); }
.link-end:hover { background: var(--accent); }
/* Square = resize, circle = connect. One shape per verb, both screen-constant. */
.rz-handle {
  position: absolute; left: 0; top: 0; width: 8px; height: 8px; margin: -4px 0 0 -4px;
  background: var(--handle-fill); border: 1px solid var(--accent); border-radius: 2px;
  transform-origin: 4px 4px; pointer-events: auto;
  box-shadow: 0 1px 2px oklch(0% 0 0 / 0.20);
  animation: fade-in var(--dur-fast);
}
.rz-handle:hover { background: var(--accent); }
.rz-h-nw, .rz-h-se { cursor: nwse-resize; }
.rz-h-ne, .rz-h-sw { cursor: nesw-resize; }
.marquee { position: absolute; border: 1px solid var(--canvas-marquee-stroke); background: var(--canvas-marquee-fill); pointer-events: none; z-index: 5; border-radius: 0; }
.canvas-hint { position: absolute; top: var(--sp-4); left: 50%; transform: translateX(-50%); background: var(--surface-overlay); border: 1px solid var(--overlay-border); padding: 7px var(--sp-3); border-radius: var(--r-md); color: var(--text-secondary); z-index: 6; pointer-events: none; box-shadow: var(--shadow-md); animation: drop-in var(--dur-base) var(--ease-out); }
.canvas-hint { transform: translateX(-50%); }
@keyframes hint-in { from { opacity: 0; translate: 0 calc(-1 * var(--motion-offset)); } }
.canvas-hint { animation-name: hint-in; }

/* ---------- cards ---------- */
/* container-type: size lets .lod-cover size its whole composition from the card
   it is in (cqw / cqh). The card already has an explicit width and height, so the
   size containment this implies changes nothing about layout. */
.card { container-type: size; contain: layout; position: absolute; left: 0; top: 0; border-radius: var(--r-lg); background: var(--surface-raised); box-shadow: var(--shadow-card); outline: 1.5px solid transparent; display: flex; flex-direction: column; overflow: hidden; transition: outline-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-base) var(--ease-out); }
.card.enter-pop { animation: card-pop var(--dur-lazy) var(--ease-spring); }
.card.enter-right { animation: card-right var(--dur-enter) var(--ease-out-expo); }
.card.enter-up { animation: card-up var(--dur-lazy) var(--ease-out); }
/* The board arriving (staggerEntrance in app.js sets each card's delay): a short rise
   in screen pixels — --px keeps it 10px at any zoom — with no overshoot. */
@keyframes card-boot { from { opacity: 0; translate: 0 calc(10 * var(--px, 1px)); scale: 0.985; } }
.card.enter-boot { animation: card-boot var(--dur-enter) var(--ease-out-expo) both; animation-delay: var(--enter-delay, 0ms); }
.card.frame.enter-boot { animation-name: fade-in; }
.viewport.board-entering :is(.links, .link-label) { animation: fade-in var(--dur-enter) var(--ease-out) 200ms both; }
/* Stepping into or out of a nested board: the camera carries the motion, so the cards
   only resolve — a fade, no pop of their own. */
.card.enter-level { animation: fade-in var(--dur-base) var(--ease-out) both; }
.card.leave { opacity: 0; scale: 0.96; transition: opacity var(--dur-base) var(--ease-in), scale var(--dur-base) var(--ease-in); pointer-events: none; }
.card { transition: outline-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-base) var(--ease-out), scale var(--dur-base) var(--ease-out); }
/* Set by cull(): the box stays (geometry and layout are unchanged), the contents stop
   being rendered. content-visibility is the cheap half of virtualisation without any of
   the scroll-position bookkeeping. A culled card is at least a viewport outside the
   camera, so its own box is not painted either: every painted box far apart from the
   rest became a compositor layer of its own, and on a 304-card board Chrome spent ~7ms
   re-layerizing those on every pan frame and ~12ms on every click (half of it gone). */
.card.offscreen { content-visibility: hidden; visibility: hidden; }
.card.lifted { scale: 1.012; box-shadow: var(--ring-selected), var(--shadow-lg); will-change: transform; }
.card.lifted .card-head { cursor: grabbing; }
.card.pulse { animation: flash-ring var(--dur-pulse) var(--ease-out); }
/* Hover is neutral; accent is reserved for states the user caused. */
/* One channel per state, so nothing stacks:
     elevation (shadow) = hovered   — the pointer is on this
     outline  (accent)  = selected  — this is the chosen one
     handles            = selected  — here is where to grab
     fill               = identity  — note colour, frame region
   Hover used to fire four of these at once. The pointer crosses many
   objects, so hover has to be the cheapest signal of the set. */
/* Hover reveals controls; selection owns the accent outline. */
.card.selected { outline-color: var(--accent); box-shadow: var(--ring-selected), var(--shadow-card); }
.card.peek-source { outline-color: var(--accent); outline-style: dashed; }
.card.link-target { outline: 2px solid var(--accent); box-shadow: 0 0 0 6px var(--accent-ring), var(--shadow-card); }
.card.flash { animation: flash-ring var(--dur-pulse) var(--ease-out); }
/* A frame that dragged cards would land in. Same accent as selection, dashed, so it
   reads as "about to" rather than "is". */
.card.frame.drop-into { background: var(--frame-fill-selected); outline-color: var(--accent); outline-style: dashed; }
/* The card behind the outline row under the pointer: a quieter version of selection. */
.card.ol-hover:not(.selected) { outline-color: color-mix(in oklch, var(--accent) 55%, transparent); }
.card.web.ol-hover::after { background: transparent; }

/* The head names the plane it is on (--head-fill) and the seam that plane
   wants (--head-seam); the rules below re-point those two per card kind, so
   the shape of the seam is written once.
   The seam is a painted rule, not a border, and it fades out before either
   rounded corner. A full-bleed 1px border reads as a CUT across the card —
   it terminates hard into the corner arc, and being in world units it lands
   on 1.21 device px at 121% zoom, which smears it across two rows. Fading
   the ends and pinning it to one screen pixel makes it a seam again. */
.card-head { --head-fill: var(--card-chrome); --head-seam: var(--card-chrome-divider); height: 32px; flex: none; display: flex; align-items: center; gap: var(--sp-2); padding: 0 6px 0 10px; background-color: var(--head-fill); background-image: linear-gradient(90deg, transparent, var(--head-seam) 7%, var(--head-seam) 93%, transparent); background-repeat: no-repeat; background-position: 0 100%; background-size: 100% 1px; color: var(--text-secondary); font-size: var(--fs-sm); letter-spacing: var(--ls-12); font-weight: var(--fw-medium); cursor: grab; transition: background-color var(--dur-fast); }
.card.selected .card-head { --head-fill: var(--card-chrome-selected); }
/* A web card's head sits on the PAGE plane: page white, page ink, a seam in
   page ink. That is what closes the dark-mode gap — the head no longer has
   an app-themed colour to disagree with the white page underneath it. */
.card.web .card-head { --head-fill: var(--card-chrome-page); --head-seam: var(--card-chrome-page-divider); color: var(--card-chrome-page-ink); }
.card.web.selected .card-head { --head-fill: var(--card-chrome-page-selected); }
.card.web .card-title { color: var(--text-page); }
.card.web .chb { color: var(--card-chrome-page-ink); }
.card.web .chb:hover { background: var(--card-chrome-page-hover); color: var(--text-page); }
.card.web .chb:active { background: var(--card-chrome-page-active); }
.card.web .rename-input { background: var(--card-chrome-page-active); color: var(--text-page); }
/* A preview card (the site refused to be framed) is Broadser's own UI, not a page:
   its head and body take the app plane, so in dark it is not a white slab. */
.card.web.no-live .card-head { --head-fill: var(--card-chrome); --head-seam: var(--card-chrome-divider); color: var(--text-secondary); }
.card.web.no-live.selected .card-head { --head-fill: var(--card-chrome-selected); }
.card.web.no-live .card-title { color: var(--text-primary); }
.card.web.no-live .chb { color: var(--text-secondary); }
.card.web.no-live .chb:hover { background: var(--state-hover); color: var(--text-primary); }
.card.web.no-live .chb:active { background: var(--state-active); }
.card.web.no-live .card-body { background: var(--surface-raised); color: var(--text-primary); }
/* A preview's last line once Broadser for Mac is out (web.js preview()): its own row in
   the foot. On the board a press on a card never follows a link, so there it's text; in
   Side Peek and the expanded page it's a link to the home page. */
.pv-mac { flex-basis: 100%; }
.pv-mac a { color: var(--text-secondary); text-decoration: underline; text-decoration-color: var(--border-strong); text-underline-offset: 2px; }
.pv-mac a:hover { color: var(--text-primary); }
.card .pv-mac a { pointer-events: none; color: inherit; text-decoration: none; }
/* A compact preview should end on complete lines, above its status row. */
.card .pv-desc { -webkit-line-clamp: 2; flex-shrink: 0; }
.card .pv-site, .card .pv-foot { flex-shrink: 0; }
.card.lifted .card-head { cursor: grabbing; }
.card-head .fav { width: var(--icon-md); height: var(--icon-md); flex: none; }
.card-head .ti svg { width: var(--icon-md); height: var(--icon-md); color: currentColor; opacity: 0.85; }
/* the head is flush with the body now, so the title carries the weight the
   old grey band used to: full-strength ink, not the secondary rung. */
.card-title { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-primary); }
.card-head-actions { display: flex; gap: 2px; opacity: 0; transition: opacity var(--dur-fast); }
.card:hover .card-head-actions, .card.selected .card-head-actions, .card-head:focus-within .card-head-actions { opacity: 1; }
.chb { width: var(--ctl-xs); height: var(--ctl-xs); border-radius: var(--r-sm); display: grid; place-items: center; color: var(--text-secondary); transition: background var(--dur-fast), color var(--dur-fast); }
.chb svg { width: var(--icon-md); height: var(--icon-md); }
.chb:hover { background: var(--state-hover); color: var(--text-primary); }
.chb:active { background: var(--state-active); }
.card-body { flex: 1; min-height: 0; position: relative; }
.card.web .card-body { background: var(--surface-page); color: var(--text-page); cursor: default; }
/* peripheral page content is veiled so app chrome stays above it in the
   hierarchy; focused reading surfaces (peek, expand) never are.
   The veil covers the whole card, title bar included — it used to sit on the
   body alone, which meant the head kept its own colour while the page under
   it dimmed, and the two could only agree at one veil strength. Over both,
   they are always the same colour, dimmed and lit together. */
.card.web::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: var(--page-veil); transition: background var(--dur-base) var(--ease-out); z-index: 2; }
/* Preview/error copy uses app colours already; only a real page needs the veil. */
.card.web.no-live::after, .card.web:hover::after, .card.web.selected::after { background: transparent; }
.page-host { overflow: hidden; }
.page-scroll { position: absolute; inset: 0; overflow: auto; overscroll-behavior: contain; animation: fade-in var(--dur-base) var(--ease-out); }
.rename-input { flex: 1; min-width: 0; background: var(--surface-sunken);  /* 这个槽开在标题栏上，而标题栏现在与卡片同面（浅色为白）；用最深的那一层才能在两种状态下都看出是个可输入的凹槽 */ border: 1px solid var(--field-focus-border); border-radius: var(--r-xs); color: var(--text-primary); padding: 2px 6px; font-size: var(--fs-sm); letter-spacing: var(--ls-12); outline: 0; box-shadow: var(--ring-focus); }
.resize { position: absolute; right: 0; bottom: 0; width: 18px; height: 18px; cursor: nwse-resize; z-index: 3; }
.resize::after { content: ""; position: absolute; right: 4px; bottom: 4px; width: 7px; height: 7px; border-right: 2px solid var(--border-strong); border-bottom: 2px solid var(--border-strong); border-radius: 1px; opacity: 0; transition: opacity var(--dur-fast); }
.card.web .resize::after { border-color: var(--on-page-ink); }
.card.selected .resize::after { opacity: 1; }

/* skeleton / loading */
.skeleton { position: absolute; inset: 0; padding: 18px; background: var(--surface-page); display: none; flex-direction: column; gap: var(--sp-3); }
.card.loading .skeleton, .peek.loading .skeleton, .skeleton.show { display: flex; }
.card.loading .card-body { visibility: hidden; }
/* A page resolves out of its skeleton instead of replacing it: the page is drawn
   underneath at once and the skeleton fades off it (display is held through the fade
   by allow-discrete; a browser without it just swaps, as before). Into loading is instant. */
.card.web .skeleton { opacity: 0; transition: opacity var(--dur-slow) var(--ease-out), display var(--dur-slow) allow-discrete; }
.card.web.loading .skeleton { opacity: 1; transition: none; }
/* Below the label threshold the label is what a card shows, loaded or not. The white
   skeleton used to cover it, so a dark board opened as a field of white boxes that
   flicked to labels one by one. */
.cards.lod .card.web.loading .skeleton { display: none; }
.peek .skeleton { position: relative; }
.sk-bar, .sk-block { border-radius: var(--r-xs); background: linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 50%, var(--skeleton-base) 75%); background-size: 200% 100%; animation: shimmer var(--dur-loading) var(--ease-in-out) infinite; }
/* the sweep only runs while something is loading — a fading skeleton holds still */
.card.web:not(.loading) .sk-bar, .card.web:not(.loading) .sk-block { animation: none; }
.sk-bar { height: 12px; } .sk-bar.w1 { width: 60%; } .sk-bar.w2 { width: 85%; } .sk-bar.w3 { width: 40%; }
.sk-block { height: 90px; }
.card.loading .card-head .fav { animation: pulse var(--dur-pulse) var(--ease-in-out) infinite; }

/* ---------------- LOD (zoomed out) ----------------
   Below the threshold a card stops being "chrome plus content" and becomes a
   single label. Three rules, in the order they matter:

   1. Say each thing once. The title bar used to print the card's title in
      small type with an ellipsis, directly above the LOD title printing the
      same string in full — two renderings of one string, which is the most
      expensive thing a thumbnail can spend its space on. The bar is hidden and
      the label takes the whole card.
   2. One structure for every kind, so the eye lands in the same place on every
      card: an identity row (icon, and the host when there is one) pinned to the
      top-left, then the title as the hero directly under it. Centred titles on
      cards of five different heights put the text at five different places;
      NN/g's card eye-tracking is explicit that irregular placement turns a
      sweep into a semi-random hunt.
   3. The fill still says what the card IS. A pinned page keeps the page plane
      (white, veiled at rest like at any other zoom); a note keeps its colour; a
      frame keeps its wash. Squint at the board and the kinds are still sorted
      without reading a word — which is the same test a favicon has to pass.

   What is dropped is decoration, never identity: the bar, the second copy of
   the favicon, the resize handles, the head actions. */

/* One proportional unit drives the whole label, so a 300x150 note and a
   660x300 page get the same composition rather than the same 34px type dropped
   onto wildly different amounts of room. min() of the two axes means a short
   wide card is governed by its height and cannot grow a title taller than
   itself, and the clamp is doing the real work at both ends: the floor is a
   legibility floor (at the 42% threshold 26 world px is ~11 screen px, and a
   small card must not be the illegible one), the ceiling stops a very large
   card from turning its name into a billboard. The band is narrow on purpose — this
   is a proportional nudge around one size, not a fluid type scale. */
.lod-cover {
  --u: clamp(26px, min(6.2cqw, 17cqh), 46px);
  position: absolute; inset: 32px 0 0 0; overflow: hidden;
  display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start;
  gap: calc(var(--u) * 0.42); padding: clamp(calc(var(--u) * 0.6), 4.2cqw, calc(var(--u) * 1.05));
  opacity: 0; pointer-events: none; transition: opacity var(--dur-slow) var(--ease-out);
  background: var(--lod-bg); color: var(--lod-fg);
}
/* The anchor is read before the title, so it has to survive the same squint the
   title does — a 16px favicon scaled down is noise, not a mark. */
.lod-id { display: flex; align-items: center; gap: calc(var(--u) * 0.3); max-width: 100%; min-width: 0; color: var(--lod-meta); }
.lod-cover .fav { width: calc(var(--u) * 0.92); height: calc(var(--u) * 0.92); border-radius: calc(var(--u) * 0.2); flex: none; }
.lod-cover .ti svg { width: calc(var(--u) * 0.92); height: calc(var(--u) * 0.92); }
.lod-host { font-size: calc(var(--u) * 0.5); font-weight: var(--fw-medium); letter-spacing: var(--ls-12); line-height: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lod-title { font-size: var(--u); font-weight: var(--fw-bold); line-height: var(--lh-tight); letter-spacing: var(--ls-34); max-width: 100%; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; }
.card.web .card-body, .card.text .text-body { transition: opacity var(--dur-slow) var(--ease-out); }
.cards.lod .lod-cover { opacity: 1; pointer-events: auto; inset: 0; }
.cards.lod .card.web .card-body, .cards.lod .card.text .text-body { opacity: 0; pointer-events: none; }
/* The cover is opaque, so below the threshold the stand-in page is not merely
   invisible, it is unreachable. Skipping its layout and paint is the difference
   between 60 pages being rendered off-screen and none of them being. */
.cards.lod .card.web .card-body, .cards.lod .card.text .text-body { content-visibility: hidden; }
/* The LOD switch used to animate height and font-size on every card at once, which
   is a layout pass per frame per card for 280ms. The crossfade on .lod-cover is what
   the eye actually reads as the transition, so the chrome geometry changes on the
   frame the threshold is crossed and the fade carries the rest. */
.card-head { transition: background var(--dur-fast); }
.cards.lod .card-head, .cards.lod .card-head-actions, .cards.lod .resize { display: none; }
.card.frame:not(.lod) .lod-cover { display: none; }
/* A pinned page is still a page at any zoom: page white, page ink, page veil.
   That is also what separates it from the canvas — the canvas is deliberately
   the deepest plane in the app precisely so a white page clears it. */
.cards.lod .card.web .lod-cover { background: var(--lod-page-bg); color: var(--lod-page-fg); }
.cards.lod .card.web .lod-id { color: var(--lod-page-meta); }
/* A page with a picture keeps it when zoomed out (has-still: the Mac's still, or the
   hosted board's saved screenshot — set by desktop.js paintHost and app.js renderWebBody).
   The label stays where every card has it, site then title at the top, now a band over
   the page; the picture fills the rest, so a board of pages is recognised by how its
   pages look and still read as one column of titles. The title gives up its third and
   fourth lines to the picture. Only a picture: the prototype's stand-in pages (whole
   documents) stay hidden as above, and a page still loading or failed keeps the label. */
.cards.lod .card.web.has-still > .lod-cover { position: relative; inset: auto; order: -1; flex: none; padding-bottom: calc(var(--u) * 0.5); box-shadow: 0 calc(1 * var(--px)) 0 var(--card-chrome-page-divider); z-index: 1; }
.cards.lod .card.web.has-still > .lod-cover .lod-title { -webkit-line-clamp: 2; }
.cards.lod .card.web.has-still > .card-body { opacity: 1; content-visibility: visible; }
/* A note is already wearing its colour; the label only needs the ink. */
.cards.lod .card.text .lod-cover { background: transparent; color: var(--text-primary); }
.cards.lod .card.text .lod-id { color: var(--note-fg); }
/* A sub-canvas holds a board, so at a distance it should read as one: the label
   sits on the canvas plane, dot texture and all. Without this it wears
   --surface-raised and is indistinguishable from a pinned page — two kinds that
   look identical is exactly the failure the fill rule exists to prevent. */
.cards.lod .card.subcanvas .lod-cover { background: var(--surface-sunken); background-image: radial-gradient(var(--grid-dot) 1px, transparent 1.2px); background-size: calc(var(--u) * 0.62) calc(var(--u) * 0.62); color: var(--text-primary); box-shadow: inset 0 0 0 calc(var(--px) * 1.5) var(--border-strong); }
/* …but the canvas plane on the canvas plane has no edge, and the card read as a
   caption floating in empty board. A window onto a board still has a frame: the
   strong hairline draws where it ends without claiming a fill of its own. */
/* Below the threshold a connector label renders at three or four screen pixels.
   It cannot be read and it sits on top of the titles that can, so it goes —
   the same reason the head, the handles and the page content go. */
.link-label { transition: opacity var(--dur-slow) var(--ease-out); }
.cards.lod ~ .link-labels { pointer-events: none; }
.cards.lod ~ .link-labels .link-label { opacity: 0; pointer-events: none; }
.cards.lod .frame-label { font-size: var(--fs-2xl); letter-spacing: var(--ls-20); top: -40px; padding: 6px var(--sp-3); }
.cards.lod .frame-label .ti svg { width: 22px; height: 22px; }

/* text card */
.card.text .card-head { background: transparent; color: var(--text-secondary); font-weight: var(--fw-semibold); }
.card.text .text-body { padding: var(--sp-1) 14px 14px; line-height: var(--lh-base); font-size: var(--fs-md); letter-spacing: var(--ls-13); color: var(--text-primary); overflow: hidden; outline: 0; cursor: default; white-space: normal; }
.card.text .text-body:empty::before { content: "Type a thought…"; color: var(--text-secondary); }
.card.text .text-body ul { padding-left: 18px; margin: var(--sp-1) 0; }
.card.text.editing { outline-color: var(--accent); box-shadow: var(--ring-selected), var(--shadow-card); }
.card.text.editing .card-head { cursor: default; }
.fmt-bar { display: none; align-items: center; gap: 2px; padding: var(--sp-1) 6px; border-top: 1px solid var(--border-default); }
.card.text.editing .fmt-bar { display: flex; animation: fade-in var(--dur-base); }
.fmt-bar button { width: var(--ctl-sm); height: var(--ctl-xs); border-radius: var(--r-sm); display: grid; place-items: center; color: var(--text-secondary); }
.fmt-bar button svg { width: var(--icon-sm); height: var(--icon-sm); }
.fmt-bar button:hover { background: var(--state-active); color: var(--text-primary); }
.fmt-bar button:active { background: var(--state-active); }
.fmt-bar button.on { background: var(--state-selected); color: var(--accent-text); }
.fmt-hint { margin-left: auto; font-size: var(--fs-xs); letter-spacing: var(--ls-11); color: var(--text-tertiary); padding-right: var(--sp-1); }
/* Content tones carry the fill, a matching border and a label colour that
   is guaranteed readable on that fill in both themes. */
.card.text.color-dark { --note-fg: var(--note-dark-fg); background: var(--note-dark-bg); --card-ring: inset 0 0 0 calc(1 * var(--px)) var(--note-dark-border); box-shadow: var(--card-elev); } .card.text.color-dark .card-head { color: var(--note-fg); }
.card.text.color-blue { --note-fg: var(--note-blue-fg); background: var(--note-blue-bg); --card-ring: inset 0 0 0 calc(1 * var(--px)) var(--note-blue-border); box-shadow: var(--card-elev); } .card.text.color-blue .card-head { color: var(--note-fg); }
.card.text.color-teal { --note-fg: var(--note-teal-fg); background: var(--note-teal-bg); --card-ring: inset 0 0 0 calc(1 * var(--px)) var(--note-teal-border); box-shadow: var(--card-elev); } .card.text.color-teal .card-head { color: var(--note-fg); }
.card.text.color-amber { --note-fg: var(--note-amber-fg); background: var(--note-amber-bg); --card-ring: inset 0 0 0 calc(1 * var(--px)) var(--note-amber-border); box-shadow: var(--card-elev); } .card.text.color-amber .card-head { color: var(--note-fg); }
.card.text.color-rose { --note-fg: var(--note-rose-fg); background: var(--note-rose-bg); --card-ring: inset 0 0 0 calc(1 * var(--px)) var(--note-rose-border); box-shadow: var(--card-elev); } .card.text.color-rose .card-head { color: var(--note-fg); }

/* frame */
/* Region = plane, not outline. No border; the fill does the work. */
.card.frame { background: var(--frame-fill); box-shadow: none; border: 0; overflow: visible; outline-offset: 2px; transition: background var(--dur-fast), outline-color var(--dur-fast); }

/* Frame fill stays stable through hover and selection. Only drop-into tints it. */
.frame-label { position: absolute; top: -28px; left: -1px; display: flex; align-items: center; gap: 6px; padding: 4px var(--sp-2) 4px 6px; border-radius: var(--r-sm); color: var(--text-secondary); font-size: var(--fs-sm); font-weight: var(--fw-semibold); letter-spacing: var(--ls-12); cursor: grab; white-space: nowrap; transition: background var(--dur-fast), color var(--dur-fast); }
.frame-label:hover, .card.frame.selected .frame-label { background: var(--state-hover); color: var(--text-primary); }
.frame-label .ti svg { width: var(--icon-sm); height: var(--icon-sm); color: currentColor; }
.frame-count { font-weight: var(--fw-medium); color: var(--text-tertiary); font-size: var(--fs-xs); letter-spacing: var(--ls-11); padding-left: 2px; }
.card.frame .resize::after { border-color: var(--border-strong); }

/* card / link toolbars */
.card-toolbar, .link-toolbar { position: absolute; z-index: var(--z-toolbar); display: flex; align-items: center; gap: 2px; padding: var(--sp-1); background: var(--surface-overlay); border: 1px solid var(--overlay-border); border-radius: var(--r-lg); box-shadow: var(--shadow-md); }
.card-toolbar.show, .link-toolbar { animation: rise-in var(--dur-base) var(--ease-out); }
.card-toolbar button, .link-toolbar button { width: var(--row-sm); height: var(--row-sm); border-radius: var(--r-sm); display: grid; place-items: center; color: var(--text-secondary); transition: background var(--dur-fast), color var(--dur-fast); }
.card-toolbar button svg, .link-toolbar button svg { width: var(--icon-md); height: var(--icon-md); }
.card-toolbar .toolbar-expand { width: auto; display: inline-flex; align-items: center; gap: 6px; padding: 0 9px; font-size: var(--fs-sm); }
.card-toolbar button:hover, .link-toolbar button:hover { background: var(--state-hover); color: var(--text-primary); }
.card-toolbar button:active, .link-toolbar button:active { background: var(--state-active); }
.card-toolbar button.danger:hover, .link-toolbar button.danger:hover { color: var(--danger-text); background: var(--danger-soft); }
.tb-sep { width: 1px; height: 18px; background: var(--border-default); margin: 0 3px; }
/* One segmented control everywhere (Settings, Appearance, Home's layout, Library's
   tabs, a connection's style): the track is a small control (--ctl-sm), so it lines
   up with the small buttons beside it, and the chosen segment is --control-thumb.
   `.seg` inside a popover or toolbar keeps these sizes (see the popover rules). */
.seg { display: flex; gap: 1px; padding: 2px; background: var(--fill-subtle); border-radius: var(--r-sm); }
.seg button { width: var(--ctl-md); height: calc(var(--ctl-sm) - 4px); border-radius: var(--r-xs); }
.seg-text { flex: none; }
.seg-text button { width: auto; padding: 0 10px; height: calc(var(--ctl-sm) - 4px); font-size: var(--fs-xs); letter-spacing: var(--ls-11); font-weight: var(--fw-medium); color: var(--text-secondary); transition: background var(--dur-fast), color var(--dur-fast); }
.seg-text button:hover { color: var(--text-primary); }
.seg-text button[aria-pressed="true"], .seg button.on { background: var(--control-thumb); color: var(--text-primary); box-shadow: var(--control-thumb-shadow); }
.pop-field { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); padding: 6px 10px 6px var(--sp-3); }
.pop-field-label { font-size: var(--fs-sm); letter-spacing: var(--ls-12); color: var(--text-secondary); }
/* ··· ▸ Background (app.js #bg-pick): a picture of each pattern on the canvas colour, the
   one in use ringed in the accent, as Settings ▸ Theme's pictures are. The pictures draw
   at a fixed small scale with a stronger ink than the board's, or they would read empty. */
.bg-field { padding: 4px 6px 4px; }
.bg-field .pop-field-label { padding: 2px 4px 8px; }
.bg-pick { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
.popover .bg-opt { min-width: 0; height: auto; flex-direction: column; justify-content: flex-start; gap: 6px; padding: 0 0 2px; border-radius: var(--r-md); }
.popover .bg-opt:not(:disabled):hover, .popover .bg-opt:not(:disabled):active { background: none; }
.bg-opt:focus-visible { outline: none; }
.bgp {
  --bgp-ink: light-dark(oklch(20% calc(0.02 * var(--chroma-n)) var(--hue-n) / 0.11), oklch(100% 0 0 / 0.09));
  --bgp-major: light-dark(oklch(20% calc(0.02 * var(--chroma-n)) var(--hue-n) / 0.22), oklch(100% 0 0 / 0.17));
  --bgp-dot: light-dark(oklch(20% calc(0.02 * var(--chroma-n)) var(--hue-n) / 0.36), oklch(100% 0 0 / 0.28));
  display: block; width: 100%; aspect-ratio: 4 / 3; border-radius: var(--r-sm);
  background-color: var(--canvas-bg); box-shadow: inset 0 0 0 1px var(--border-default);
  transition: box-shadow var(--dur-fast) var(--ease-out);
}
.bgp[data-bg="dots"] { background-image: radial-gradient(circle at center, var(--bgp-dot) 0 0.7px, transparent 1.2px); background-size: 6px 6px; background-position: center; }
.bgp[data-bg="grid"] {
  background-image: linear-gradient(to right, var(--bgp-major) 1px, transparent 1px), linear-gradient(to bottom, var(--bgp-major) 1px, transparent 1px),
    linear-gradient(to right, var(--bgp-ink) 1px, transparent 1px), linear-gradient(to bottom, var(--bgp-ink) 1px, transparent 1px);
  background-size: 24px 24px, 24px 24px, 6px 6px, 6px 6px; background-position: 2px 2px;
}
.bgp[data-bg="lines"] { background-image: linear-gradient(to bottom, var(--bgp-major) 1px, transparent 1px); background-size: 100% 6px; background-position: 0 2px; }
.bg-opt:not(:disabled):not([aria-checked="true"]):hover .bgp { box-shadow: inset 0 0 0 1px var(--border-strong); }
.bg-opt[aria-checked="true"] .bgp { box-shadow: inset 0 0 0 1px var(--border-subtle), 0 0 0 2px var(--accent-line); }
.bg-opt:focus-visible .bgp { outline: 2px solid var(--accent-line); outline-offset: 2px; }
.bg-opt[aria-checked="true"]:focus-visible .bgp { outline: none; }
.bg-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; font-size: var(--fs-xs); letter-spacing: var(--ls-11); line-height: var(--lh-snug); color: var(--text-secondary); }
.bg-opt[aria-checked="true"] .bg-name { color: var(--text-primary); font-weight: var(--fw-medium); }
/* Someone who can only look at the board sees its background, but doesn't choose it. */
.board-viewonly :is(.bg-field, .bg-field + .pop-sep) { display: none; }
.link-toolbar button.on { background: var(--state-selected); color: var(--text-primary); }
.link-toolbar .seg button.on { background: var(--control-thumb); box-shadow: var(--control-thumb-shadow); }
.link-toolbar button.flip .mini { transform: scaleX(-1); }
.mini { width: 22px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.mini .f { fill: currentColor; stroke: none; }
.lsw-default { --swatch-color: var(--link-stroke); } .lsw-blue { --swatch-color: var(--note-blue-solid); } .lsw-teal { --swatch-color: var(--note-teal-solid); } .lsw-amber { --swatch-color: var(--note-amber-solid); } .lsw-rose { --swatch-color: var(--note-rose-solid); }
.tb-count { padding: 0 var(--sp-2) 0 6px; font-size: var(--fs-sm); letter-spacing: var(--ls-12); color: var(--text-secondary); font-weight: var(--fw-medium); white-space: nowrap; }
.swatches { display: flex; gap: var(--sp-1); padding: 0 var(--sp-1); }
.card-toolbar .sw { width: 18px; height: 18px; border-radius: 50%; border: 2px solid transparent; transition: transform var(--dur-fast) var(--ease-spring); }
.card-toolbar .sw:hover { outline: 1px solid var(--text-secondary); outline-offset: 3px; }
.card-toolbar .sw.on { border-color: var(--text-primary); }
.sw-dark { background: var(--note-dark-solid); } .sw-blue { background: var(--note-blue-solid); } .sw-teal { background: var(--note-teal-solid); } .sw-amber { background: var(--note-amber-solid); } .sw-rose { background: var(--note-rose-solid); }

/* empty state */
.empty { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: var(--sp-10); z-index: 4; pointer-events: none; animation: fade-in var(--dur-slow); }
/* The sidebar floats over the canvas: centre the start in what is left beside it. */
.app.outline-open .empty { padding-left: calc(var(--outline-w, 260px) + var(--sp-10)); }
.empty > * { pointer-events: auto; animation: rise-in var(--dur-slow) var(--ease-out) both; }
.empty > *:nth-child(2) { animation-delay: 60ms; } .empty > *:nth-child(3) { animation-delay: 120ms; } .empty > *:nth-child(4) { animation-delay: 180ms; } .empty > *:nth-child(5) { animation-delay: 260ms; }
.empty h2 { margin: 0 0 var(--sp-2); font-size: var(--fs-2xl); font-weight: var(--fw-semibold); letter-spacing: var(--ls-20); line-height: var(--lh-tight); }
.empty p { margin: 0 0 22px; color: var(--text-secondary); max-width: 420px; line-height: var(--lh-relaxed); font-size: var(--fs-md); letter-spacing: var(--ls-13); }
.empty-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--sp-2); }
.empty-actions .text-btn { height: var(--ctl-lg); padding: 0 var(--sp-4); }
.empty-hint { margin-top: 26px; color: var(--text-secondary); font-size: var(--fs-sm); letter-spacing: var(--ls-12); }
.empty-art { position: relative; width: 180px; height: 110px; margin-bottom: 22px; }
.empty-art span { position: absolute; border-radius: var(--r-md); background: var(--surface-raised); border: 1px solid var(--border-default); box-shadow: var(--shadow-card); }
.empty-art span:nth-child(1) { left: 0; top: 10px; width: 90px; height: 70px; background: var(--lod-bg); }
.empty-art span:nth-child(2) { left: 70px; top: 30px; width: 100px; height: 76px; background: linear-gradient(var(--fill-subtle), var(--fill-subtle)), var(--canvas-bg); } /* opaque: it overlaps the first */
.empty-art span:nth-child(3) { left: 118px; top: 0; width: 62px; height: 46px; background: var(--surface-overlay); }
.empty-art span::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 12px; border-radius: var(--r-md) var(--r-md) 0 0; background: var(--card-chrome); border-bottom: 1px solid var(--card-chrome-divider); }
.empty-art span:nth-child(2)::before { background: transparent; }

/* expanded card */
/* Browser presentation changes preserve attached documents and their native bounds. */
.expand { position: fixed; inset: 0; z-index: calc(var(--z-chrome) + 1); background: var(--surface-raised); display: flex; flex-direction: column; overflow: hidden; transform-origin: 0 0; user-select: text; }
.expand-head { --reader-bg: var(--card-chrome-page); --reader-ink: var(--text-page); --reader-muted: color-mix(in oklab, var(--reader-ink), var(--reader-bg) 35%); --return-bg: color-mix(in oklab, var(--reader-bg), var(--reader-ink) 5%); --return-seam: color-mix(in oklab, var(--reader-ink), transparent 88%); height: 44px; min-height: 44px; flex: none; display: flex; border-bottom: 1px solid var(--return-seam); background: var(--reader-bg); color: var(--reader-ink); }
.expand-return-zone { position: relative; z-index: 1; flex: none; align-self: stretch; display: flex; align-items: center; padding: 4px 12px; background: var(--return-bg); border-bottom-right-radius: 16px; box-shadow: inset -1px -1px 0 var(--return-seam); }
/* A small concave shoulder at the top and a rounded foot below make the return
   area a tab, while the browser controls stay on the page's uninterrupted fill. */
.expand-return-zone::after { content: ""; position: absolute; pointer-events: none; left: calc(100% - 1px); top: 0; width: 14px; height: 14px; background: radial-gradient(circle at 14px 14px, transparent 13px, var(--return-seam) 13px 14px, var(--return-bg) 14px); }
.expand-browser { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; padding: 4px 12px 4px 18px; }
.expand-head .icon-btn, .expand-return, .expand-head .expand-source { color: var(--reader-muted); }
.expand-head .icon-btn:hover, .expand-return:hover, .expand-head .expand-source:hover { background: color-mix(in oklab, var(--reader-ink), transparent 93%); color: var(--reader-ink); }
.expand-head .peek-url { max-width: 1100px; margin-inline: auto; height: 32px; }
.expand-head .peek-url { background: var(--return-bg); }
.expand-head .peek-url:hover { border-color: var(--return-seam); }
.expand-head .peek-url-text .host, .expand-address-form input { color: var(--reader-ink); }
.expand-head .peek-url-text .path { color: var(--reader-muted); }
.expand-head .peek-url-text { flex: 1; cursor: text; }
.expand-head .icon-btn { width: 32px; height: 32px; flex: none; }
.expand-return { flex: none; display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 8px; border-radius: var(--r-md); color: var(--reader-muted); font-size: var(--fs-sm); font-weight: var(--fw-medium); white-space: nowrap; }
.expand-return:hover { color: var(--reader-ink); background: color-mix(in oklab, var(--reader-ink), transparent 93%); }
.expand-return:active { background: color-mix(in oklab, var(--reader-ink), transparent 88%); }
.expand-browser > .peek-nav { flex: none; }
.expand-source, .expand-pin { flex: none; display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 8px; white-space: nowrap; font-size: var(--fs-sm); border-radius: var(--r-md); }
.expand-source { color: var(--text-secondary); }
.expand-source:hover { background: var(--state-hover); color: var(--text-primary); }
.expand-pin { background: var(--action-tonal-bg); color: var(--action-tonal-fg); font-weight: var(--fw-medium); }
.expand-pin:hover { background: var(--action-tonal-bg-hover); }
.expand-source .pi svg, .expand-pin .pi svg { width: 16px; height: 16px; }
.expand-more, .expand-menu-label { display: none; }
.expand-menu { width: 240px; max-width: calc(100vw - 16px); }
.expand-menu button { width: 100%; height: 44px; min-height: 44px; justify-content: flex-start; gap: 10px; padding-inline: 12px; margin: 0; background: transparent; color: var(--text-primary); font-size: var(--fs-sm); font-weight: var(--fw-regular); }
.expand-menu .icon-btn { width: 100%; height: 44px; }
.expand-menu .expand-action-label { display: none; }
.expand-menu .expand-menu-label { display: inline; }
.expand-menu button:disabled { color: var(--text-tertiary); }
.expand-body { flex: 1; min-height: 0; position: relative; background: var(--surface-page); color: var(--text-page); }
.expand-page { position: absolute; inset: 0; }
.expand-page:has(> .pv:not(.pv-loading)) { display: grid; place-items: center; overflow: auto; padding: 40px 24px; background: var(--surface-base); }
.expand-page > .pv:not(.pv-loading) { position: relative; inset: auto; width: 100%; max-width: 640px; overflow: visible; background: transparent; animation: none; }
.expand-page .pv-cover { flex: none; height: 260px; border-radius: var(--r-md); }
.expand-page .pv-body { padding: 24px 0; }
.expand-page .pv:not(.has-cover) .pv-body { padding: 0; }
.expand-page .pv-foot { align-items: center; margin-top: 12px; padding-top: 16px; }
.expand-original { display: inline-flex; align-items: center; justify-content: center; min-height: 36px; padding: 0 12px; border-radius: var(--r-sm); background: var(--action-tonal-bg); color: var(--action-tonal-fg); font-size: var(--fs-sm); font-weight: var(--fw-medium); }
.expand-original:hover { background: var(--action-tonal-bg-hover); }
.expand-address-form { flex: 1; min-width: 0; }
.expand-address-form input { width: 100%; min-width: 0; border: 0; padding: 0; background: transparent; color: var(--reader-ink); font-size: var(--fs-sm); outline: none; }
.expand-head .peek-url:has(input:focus) { border-color: var(--field-focus-border); box-shadow: var(--ring-focus); }
.expand-head .peek-url-text:focus-visible { outline: none; }
.expand-body .pg { max-width: 1100px; margin: 0 auto; }
.expand-collapse { position: absolute; right: var(--sp-4); bottom: var(--sp-4); width: var(--ctl-float); height: var(--ctl-float); border-radius: var(--r-md); background: var(--on-page-scrim); color: var(--on-page-fg); display: grid; place-items: center; box-shadow: var(--shadow-md); transition: background var(--dur-fast); }
.expand-collapse:hover { background: var(--on-page-scrim-hover); }
.expand-collapse svg { width: var(--icon-lg); height: var(--icon-lg); }
.app.expanded .card-toolbar, .app.expanded .link-toolbar { display: none !important; }
/* Both presentations use the attached tab panels. The expanded chrome and tab strip
   occupy separate lanes; only the body and strip accept pointer events here. */
.app.expanded { --browser-head: 44px; --browser-tabs: 0px; }
.app.expanded.browser-multi { --browser-tabs: 40px; }
.app.expanded .expand { padding-top: var(--browser-tabs); pointer-events: none; overflow: visible; }
.app.expanded .expand :is(.expand-head, .expand-menu) { pointer-events: auto; }
.app.expanded .peek { position: fixed; inset: 0; width: auto !important; max-width: none; border: 0; border-radius: 0; box-shadow: none; background: transparent; z-index: calc(var(--z-chrome) + 2); pointer-events: none; container-type: normal; }
.app.expanded .peek > :is(.peek-tabs-row, .peek-body) { margin-left: 0; pointer-events: auto; }
.app.expanded .peek-tabs-row { background: var(--surface-chrome); }
.app.expanded .peek-body { margin-top: var(--browser-head); background: var(--surface-page); }
.app.expanded .peek :is(.peek-head, .peek-foot, .peek-resize, #peek-close, .peek-tabs-sep) { display: none; }
.app.expanded .expand-body { visibility: hidden; }
.app.expanded .expand-menu { z-index: calc(var(--z-chrome) + 3); }
.app.expanded:has(.expand-menu:not([hidden])) .peek-body { pointer-events: none; }
.expand.on-newtab #expand-url { visibility: hidden; }
@media (max-width: 640px), (pointer: coarse) { .app.expanded { --browser-head: calc(52px + env(safe-area-inset-top)); } }

@media (max-width: 1100px) {
  .expand-head .expand-action-label { display: none; }
  .expand-head .expand-source, .expand-head .expand-pin { width: 32px; padding: 0; justify-content: center; }
}
@media (max-width: 640px), (pointer: coarse) {
  .expand-head { height: calc(52px + env(safe-area-inset-top)); min-height: 52px; padding-top: env(safe-area-inset-top); }
  .expand-return-zone { padding: 4px 8px; }
  .expand-browser { padding: 4px 8px 4px 12px; gap: 4px; }
  .expand-return, .expand-head .icon-btn, .expand-head .expand-source, .expand-head .expand-pin { width: 44px; height: 44px; padding: 0; justify-content: center; }
  .expand-return { width: 48px; }
  .expand-head .peek-url { height: 44px; }
  .expand-original { min-height: 44px; }
}
@media (max-width: 640px) {
  .expand-browser > .peek-actions { display: none; }
  .expand-more { display: grid; }
  .expand-browser > .peek-nav { padding-left: 0; }
  /* Media-query layout takes effect before the menu relocation callback runs. */
  .expand-browser > .peek-nav #expand-forward { display: none; }
}

/* present */
.present-exit { position: absolute; top: var(--sp-4); right: var(--sp-4); z-index: 12; display: flex; align-items: center; gap: 10px; padding: 6px 6px 6px var(--sp-3); background: var(--surface-overlay); border: 1px solid var(--overlay-border); border-radius: var(--r-md); color: var(--text-secondary); font-size: var(--fs-sm); letter-spacing: var(--ls-12); box-shadow: var(--shadow-md); animation: drop-in var(--dur-base) var(--ease-out); }
.present-where { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }
.present-nav { display: flex; gap: 2px; }
.present-exit > button { display: flex; align-items: center; gap: 6px; height: 26px; padding: 0 var(--sp-2) 0 10px; border-radius: var(--r-sm); background: var(--fill-subtle); color: var(--text-primary); font-weight: var(--fw-medium); }
.present-exit > button:hover { background: var(--fill-strong); }
/* a board shared as a web page (app.js VIEW): where it was made, quiet beside the steps */
.present-exit .view-made { display: flex; align-items: center; height: 26px; padding: 0 var(--sp-2); border-radius: var(--r-sm); color: var(--text-tertiary); text-decoration: none; white-space: nowrap; }
.present-exit .view-made:hover { background: var(--fill-subtle); color: var(--text-primary); }
@media (max-width: 720px) {
  .present-exit { left: var(--sp-3); right: var(--sp-3); flex-wrap: wrap; gap: 4px 8px; padding: 6px 8px; }
  .present-exit .present-where { flex: 1 0 100%; max-width: 100%; }
  .present-exit .present-nav { margin-right: auto; }
  .present-exit .present-nav button, .present-exit > button, .present-exit .view-made { min-width: 44px; height: 44px; }
  .present-exit .view-made { font-size: var(--fs-xs); }
}
.app.present .topbar, .app.present .outline, .app.present .bottombar, .app.present .card-toolbar, .app.present .link-toolbar { display: none !important; }
.app.present .card-head-actions, .app.present .resize { display: none; }

/* bottom bar */
.bottombar { height: 44px; flex: none; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 0 var(--sp-3); background: var(--surface-chrome); border-top: 1px solid var(--border-default); position: relative; z-index: var(--z-chrome); }
.bl { display: flex; align-items: center; }
.br { display: flex; align-items: center; justify-content: flex-end; }
.bar-ctl { display: flex; align-items: center; gap: 2px; }
/* The tool row: the bottom band's own controls (no floating layer), the armed tool lit
   the way the minimap toggle is. Shape carries a narrow chevron for its kind; Add holds
   the rest (image, nested board, templates). */
.tool-dock { display: flex; align-items: center; gap: 2px; }
.tool-dock .icon-btn svg { width: var(--icon-lg); height: var(--icon-lg); }
.tool-dock .icon-btn.on { box-shadow: inset 0 0 0 1px var(--border-default); }
.tool-pair { display: flex; align-items: center; }
.tool-kind { width: 14px; height: var(--ctl-md); margin-left: -1px; border-radius: var(--r-xs); display: grid; place-items: center; color: var(--text-tertiary); transition: background var(--dur-fast), color var(--dur-fast); }
.tool-kind:hover, .tool-kind[aria-expanded="true"] { background: var(--state-hover); color: var(--text-primary); }
.tool-kind svg { width: var(--icon-xs); height: var(--icon-xs); transition: transform var(--dur-base) var(--ease-out); }
.tool-kind[aria-expanded="true"] svg { transform: rotate(180deg); }
#add-sub button.current { background: var(--state-selected); }
/* A locked card: no resize handles, and a padlock — with the selection, or under the
   pointer — that unlocks it. Screen-constant, in the corner a card's own controls leave free. */
.card.locked .rz { display: none; }
.card.locked .card-head, .card.locked .frame-label { cursor: default; } /* it won't move, so it doesn't offer to */
.lock-badge { position: absolute; right: calc(6 * var(--px)); bottom: calc(6 * var(--px)); z-index: 6; width: calc(22 * var(--px)); height: calc(22 * var(--px)); border-radius: calc(6 * var(--px)); display: grid; place-items: center; color: var(--text-secondary); background: var(--surface-raised); box-shadow: var(--shadow-sm); opacity: 0; pointer-events: none; transition: opacity var(--dur-fast), color var(--dur-fast); }
.lock-badge svg { width: calc(13 * var(--px)); height: calc(13 * var(--px)); }
.card.locked.selected .lock-badge, .card.locked:hover .lock-badge { opacity: 1; pointer-events: auto; }
.lock-badge:hover { color: var(--text-primary); }
.cards.lod .lock-badge, .app.present .lock-badge { display: none; }
/* Phone width keeps making and talking: a page, a note, stickers, comments, and + for the
   rest; Select (a tap is one), shapes and frames wait for a wider screen. */
@media (max-width: 720px) { .tool-dock .tool-pair, .tool-dock [data-tool="frame"] { display: none; } }
.bar-ctl .icon-btn.on { background: var(--state-selected); box-shadow: inset 0 0 0 1px var(--border-default); }
.zoom-pct { height: var(--ctl-sm); padding: 0 6px; border-radius: var(--r-sm); color: var(--text-secondary); font-variant-numeric: tabular-nums; min-width: 48px; text-align: center; font-size: var(--fs-sm); letter-spacing: var(--ls-12); font-weight: var(--fw-medium); transition: background var(--dur-fast), color var(--dur-fast); }
.zoom-pct:hover { background: var(--state-hover); color: var(--text-primary); }
.vsep { width: 1px; height: 16px; background: var(--border-default); margin: 0 var(--sp-1); }
/* ---------- canvas scrollbars ----------
   An infinite canvas has no document to measure, so the track stands for the
   union of everything on the board and wherever the camera currently is — the
   same extent the minimap draws, so the two can never disagree. Pan past the
   last card and the extent grows to include the empty space you moved into,
   which is what makes the thumb shrink and slide to the end instead of pinning.

   Figma's canvas scrollbars are the model, with one deliberate departure: the
   most common complaint about them is that you have to aim precisely to grab
   one. The painted bar here is the same 8px hairline, but the hit target is
   22px and reaches the very edge of the viewport, so throwing the pointer at
   the edge catches it. Visible weight and clickable weight are separate
   problems and only the first one wants to be small. */
.scrollbar {
  position: absolute; z-index: var(--z-canvas-ui);
  opacity: 0; transition: opacity var(--dur-slow) var(--ease-out);
  /* The TRACK never takes the pointer. A full-length strip along two edges of
     the canvas would be 22px of board you can no longer click a card in, and —
     worse — it would swallow the start of a marquee dragged out of the
     bottom-right corner, which is an ordinary gesture. The thumb takes the
     pointer instead, and carries the fat hit area on itself. Clicking the empty
     track to jump is the one thing given up; on a canvas with no pages to step
     through it was the weakest of the three behaviours anyway. */
  pointer-events: none;
  /* A hairline at rest, macOS-overlay weight: 6px, 4px clear of every edge, and
     only on hover or while held does it thicken to 10px to be grabbed. The 8px
     bar 3px off the edge read as a gutter rather than a readout. */
  --sb-bar: 6px; --sb-lip: 4px; --sb-end: 16px;
}
/* Shown while the camera is moving, while the pointer is on the thumb, and for
   as long as one is being dragged. Idle, this is not information the user
   asked for. In quickly, out slowly. */
.scrollbar.on, .scrollbar.dragging, .scrollbar:has(.sb-thumb:hover) { opacity: 1; transition-duration: var(--dur-fast); }
.scrollbar.dragging, .scrollbar:has(.sb-thumb:hover) { --sb-bar: 10px; }
/* Nothing to scroll on that axis — the whole board is in view — then nothing to
   show: a thumb the length of its track is a stripe along the edge, not a position. */
.scrollbar.full { visibility: hidden; }
.app.present .scrollbar { opacity: 0 !important; }
.sb-x { left: var(--sb-lip); right: var(--sb-end); bottom: 0; height: calc(var(--sb-bar) + var(--sb-lip)); }
.sb-y { top: var(--sb-lip); bottom: var(--sb-end); right: 0; width: calc(var(--sb-bar) + var(--sb-lip)); }
/* The minimap owns the bottom-left corner, above its button; the horizontal track
   starts after it rather than sliding a thumb underneath a panel. */
.viewport.has-minimap .sb-x { left: calc(228px + var(--sp-3) * 2); }
.sb-thumb {
  position: absolute; border-radius: var(--r-full); pointer-events: auto;
  /* the hairline of the other polarity keeps the bar legible where it crosses a
     white page in the dark theme, or the dark canvas in the light one */
  background: var(--sb-thumb); box-shadow: 0 0 0 0.5px var(--sb-ring);
  transition: background var(--dur-fast) var(--ease-out);
}
/* Visible weight and grabbable weight are separate problems and only the first
   one wants to be small. The painted bar is Figma's 8px hairline; the target is
   ~20px and runs to the very edge of the viewport, so throwing the pointer at
   the edge catches the thumb instead of sliding past it — which is the most
   common complaint about Figma's own canvas scrollbars. */
.sb-thumb::before { content: ""; position: absolute; }
.sb-x .sb-thumb { left: 0; bottom: var(--sb-lip); height: var(--sb-bar); will-change: transform; transition: background var(--dur-fast) var(--ease-out), height var(--dur-base) var(--ease-out); }
.sb-x .sb-thumb::before { inset: -10px -2px calc(var(--sb-lip) * -1) -2px; }
.sb-y .sb-thumb { top: 0; right: var(--sb-lip); width: var(--sb-bar); will-change: transform; transition: background var(--dur-fast) var(--ease-out), width var(--dur-base) var(--ease-out); }
.sb-y .sb-thumb::before { inset: -2px calc(var(--sb-lip) * -1) -2px -10px; }
.sb-thumb:hover { background: var(--sb-thumb-hover); }
.scrollbar.dragging .sb-thumb { background: var(--sb-thumb-active); }
/* Nothing on the edge competes with a card drag or a marquee in progress. */
.viewport.dragging .sb-thumb, .viewport.panning .sb-thumb { pointer-events: none; }
@media (pointer: coarse) { .scrollbar { --sb-bar: 8px; } .sb-x .sb-thumb::before { inset: -14px -4px calc(var(--sb-lip) * -1) -4px; } .sb-y .sb-thumb::before { inset: -4px calc(var(--sb-lip) * -1) -4px -14px; } }

.minimap { position: absolute; left: var(--sp-3); bottom: var(--sp-3); z-index: var(--z-canvas-ui); background: var(--surface-overlay); border: 1px solid var(--overlay-border); border-radius: var(--r-md); padding: var(--sp-1); cursor: crosshair; box-shadow: var(--shadow-sm); animation: rise-in var(--dur-base) var(--ease-out); }
/* The sidebar floats over the same corner (and sits above it): step out beside it. */
.app.outline-open .minimap { left: calc(var(--outline-w, 260px) + var(--sp-3)); }
/* Side Peek sits over the right of the canvas; what lives on that edge moves
   out from under it instead of being covered. */
.app.peek-open .sb-y { right: var(--peek-w, 560px); }
.app.peek-open .sb-x { right: calc(var(--peek-w, 560px) + var(--sb-end)); }
.app.outline-open .sb-x { left: calc(var(--outline-w, 260px) + var(--sb-lip)); }
.app.outline-open .viewport.has-minimap .sb-x { left: max(calc(var(--outline-w, 260px) + var(--sb-lip)), calc(228px + var(--sp-3) * 2)); }
/* So do the comment coach and the comments panel, as Ask AI does (ai.css): over the
   panel they hid its live page (a native view gives way to anything drawn on it), and
   the coach is about placing a comment on the board anyway. */
@media (min-width: 721px) {
  .app.peek-open .engagement-coach { left: calc((100% - var(--peek-w, 560px)) / 2); max-width: calc(100% - var(--peek-w, 560px) - 32px); }
  .app.peek-open .engagement-panel { right: calc(var(--peek-w, 560px) + var(--sp-3) * 2); }
}
.minimap canvas { display: block; border-radius: var(--r-xs);  /* 同心：--r-md 7 − sp-1 4 */ }

/* ---------- side peek ---------- */
/* The drawer is anchored to the right edge and floats clear of the chrome above and
   below it. Bottom used to be 0, so the footnote row butted straight into the bottom
   bar: three hairlines stacked with nothing between them, and the panel read as a
   piece of the window frame instead of a sheet slid in over the board. */
.peek { position: absolute; top: var(--sp-3); right: 0; bottom: var(--sp-3); width: 560px; max-width: calc(100% - 24px); z-index: var(--z-toolbar); background: var(--surface-raised); border: 1px solid var(--overlay-border); border-right: 0; border-radius: var(--r-lg) 0 0 var(--r-lg); box-shadow: var(--shadow-lg); display: flex; flex-direction: column; overflow: hidden; }
/* Visibility changes as one surface. The desktop's native page cannot share a DOM
   slide or fade, and an offscreen panel can make focus scroll the whole window. */
/* Reserve a real lane beside the content: native Mac pages must never cover the
   resize target. The quiet mark makes both edges discoverable without hovering. */
.outline, .peek { --panel-grip: 14px; }
.panel-resize { position: absolute; top: 0; bottom: 0; width: var(--panel-grip); z-index: 3; cursor: col-resize; touch-action: none; user-select: none; }
.outline-resize { right: 0; }
.peek-resize { left: 0; }
.panel-resize::after { content: ""; position: absolute; left: calc(50% - 1.5px); top: calc(50% - 20px); width: 3px; height: 40px; border-radius: var(--r-full); background: var(--border-strong); opacity: .5; transition: opacity var(--dur-fast), background var(--dur-fast); }
:is(.outline, .peek):hover .panel-resize::after { opacity: .8; }
.panel-resize:hover::after, .panel-resize:focus-visible::after, .resizing .panel-resize::after { opacity: 1; background: var(--accent); }
.panel-resize:focus-visible { outline: 2px solid var(--accent-line); outline-offset: -3px; border-radius: var(--r-sm); }
.outline > :is(.outline-head, .outline-list, .outline-foot) { margin-right: var(--panel-grip); }
.peek > :is(.peek-tabs-row, .peek-head, .peek-body, .peek-foot) { margin-left: var(--panel-grip); }
.outline.resizing, .peek.resizing { transition: none; user-select: none; }
.panel-resize-shield { position: fixed; inset: 0; z-index: var(--z-modal); cursor: col-resize; touch-action: none; }
.app.panel-resizing, .app.panel-resizing * { cursor: col-resize !important; user-select: none !important; }
@media (pointer: coarse) { .outline, .peek { --panel-grip: 28px; } }
.peek.wide { width: 880px; }
.peek { container-type: inline-size; }
.peek-head { height: 48px; flex: none; display: flex; align-items: center; gap: var(--sp-2); padding: 0 var(--sp-2) 0 var(--sp-3); border-bottom: 1px solid var(--border-subtle); }
.peek-url { flex: 1; min-width: 0; display: flex; align-items: center; gap: var(--sp-2); height: var(--ctl-float); padding: 0 4px 0 10px; background: var(--fill-subtle); border: 1px solid transparent; border-radius: var(--r-md); transition: border-color var(--dur-fast); }
.peek-url:hover { border-color: var(--border-default); }
.peek-url .peek-nav { margin-left: auto; flex: none; }
.peek-url-text .scheme { color: var(--text-tertiary); }
.peek-url .fav { width: var(--icon-md); height: var(--icon-md); }
.peek-url-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: var(--fs-sm); letter-spacing: var(--ls-12); }
.peek-url-text .host { color: var(--text-primary); } .peek-url-text .path { color: var(--text-tertiary); }
.peek-nav, .peek-actions { display: flex; gap: 2px; align-items: center; }
.peek-actions { flex: none; }
#peek-pin.on-canvas, #peek-pin.on-canvas:hover { color: var(--text-primary); }
#peek-reader.on { color: var(--text-primary); background: var(--state-selected); }
.peek-body.reader .page-scroll { background: var(--reader-bg); }
.peek-body.reader .pg { max-width: 620px; margin: 0 auto; font-size: var(--fs-xl); letter-spacing: var(--ls-16); line-height: var(--lh-relaxed); padding: var(--sp-4) 0; }
.peek-body.reader .wiki-top, .peek-body.reader .wiki-tabs, .peek-body.reader .wiki-lang, .peek-body.reader .wiki-toc, .peek-body.reader .g-top, .peek-body.reader .ca-lang, .peek-body.reader .ca-sig, .peek-body.reader .ca-menubar, .peek-body.reader .ca-crumbs, .peek-body.reader .ca-gcfoot, .peek-body.reader .ca-details, .peek-body.reader .li-nav, .peek-body.reader .ge-nav, .peek-body.reader .mi-side { display: none; }
.peek-body.reader .wiki-cols { flex-direction: column; } .peek-body.reader .wiki-box { width: 100%; }
.peek-body { flex: 1; background: var(--surface-page); color: var(--text-page); position: relative; }
.peek-foot { flex: none; display: flex; align-items: center; gap: var(--sp-2); padding: 7px var(--sp-3); border-top: 1px solid var(--border-subtle); color: var(--text-tertiary); font-size: var(--fs-xs); letter-spacing: var(--ls-11); white-space: nowrap; }
/* The state used to be an amber chip that never changed — warning colour spent on a
   neutral fact, and a lie once the page was pinned. It is a word now, and it flips. */
.peek-state { flex: none; color: var(--text-secondary); font-weight: var(--fw-medium); }
.peek-state.on-canvas { color: var(--success-text); }
.peek-from { display: flex; align-items: center; gap: 6px; min-width: 0; }
/* Tonal, not solid. Side Peek opens on every link click, so a filled accent button
   would put the app's one solid on screen almost permanently — spent on a standing
   suggestion, while the accent the user actually caused (the selected card's ring,
   an anchor being dragged) is out on the board competing with it. Reading is what
   this panel is for; pinning is an offer. Tonal keeps the label findable without
   claiming the view's primary slot. */
.peek-pin-btn { display: flex; align-items: center; gap: 6px; height: var(--ctl-sm); padding: 0 10px 0 var(--sp-2); border-radius: var(--r-sm); background: var(--action-tonal-bg); color: var(--action-tonal-fg); font-weight: var(--fw-semibold); font-size: var(--fs-sm); letter-spacing: var(--ls-12); white-space: nowrap; transition: background var(--dur-fast), transform var(--dur-fast); }
.peek-pin-btn:hover { background: var(--action-tonal-bg-hover); }
.peek-pin-btn:active { background: var(--state-active); }
.peek-pin-btn .pi svg { width: var(--icon-md); height: var(--icon-md); }
/* Already on the board: this is navigation, not a create action — one rung quieter. */
.peek-pin-btn.on-canvas { background: none; color: var(--text-secondary); }
.peek-pin-btn.on-canvas:hover { background: var(--state-hover); color: var(--text-primary); }
.peek-pin-btn.on-canvas .pi svg { color: currentColor; }

/* ---------- popovers ---------- */
.popover { position: fixed; z-index: var(--z-popover); min-width: 220px; padding: var(--sp-1); background: var(--surface-overlay); border: 1px solid var(--overlay-border); border-radius: var(--r-lg); box-shadow: var(--shadow-lg); animation: rise-in var(--dur-base) var(--ease-out); }
.popover.closing { animation: sink-out var(--dur-fast) var(--ease-in) forwards; pointer-events: none; }
.popover button, .pop-item { display: flex; align-items: center; gap: 10px; width: 100%; height: var(--row-md); padding: 0 10px; border-radius: var(--r-sm); color: var(--text-primary); text-align: left; white-space: nowrap; transition: background var(--dur-fast); }
.popover button:not(:disabled):hover, .pop-item:not(:disabled):hover { background: var(--state-hover); }
/* A menu item that cannot run says so and stops responding to the pointer —
   it is still listed so the menu does not reshuffle between openings. */
.popover button:disabled, .ctx-menu button:disabled { opacity: var(--state-disabled-opacity); }
.popover button:disabled:hover, .ctx-menu button:disabled:hover { background: none; }
.popover button:not(:disabled):active, .pop-item:not(:disabled):active { background: var(--state-active); }
.popover button.danger:hover { background: var(--danger-soft); color: var(--danger-text); }
.popover .pi { width: var(--icon-md); display: grid; place-items: center; }
.popover .pi svg { width: var(--icon-md); height: var(--icon-md); color: var(--text-secondary); }
.popover button.danger:hover .pi svg { color: var(--danger-text); }
.popover kbd { margin-left: auto; }
/* A segmented control in a menu is the same control as in Settings, not three menu
   rows: menu-row height and corner would make it a third size. */
.popover .seg button { width: auto; height: calc(var(--ctl-sm) - 4px); padding: 0 10px; border-radius: var(--r-xs); }
.popover .seg button:not([aria-pressed="true"]):not(.on):hover { background: none; }
.menu-right { margin-left: auto; display: flex; align-items: center; gap: var(--sp-2); }
.menu-right .chev-r svg { width: var(--icon-xs); height: var(--icon-xs); color: var(--text-tertiary); }
.popover .menu-right kbd { margin-left: 0; }
.add-menu { min-width: 236px; }
.add-menu button.soon { color: var(--text-secondary); }
.add-menu button.soon .pi svg { color: var(--text-tertiary); }
/* A fixed box, like a keycap: with padding round inline text it drew 16px in a title and 19px in a menu row.
   A label, not an action, so it is tonal: the solid accent belongs to the view's one primary button. */
.beta { display: inline-flex; align-items: center; height: 16px; font-size: var(--fs-2xs); line-height: 1; font-weight: var(--fw-semibold); color: var(--action-tonal-fg); background: var(--action-tonal-bg); padding: 0 5px; border-radius: var(--r-xs); letter-spacing: var(--ls-10); }
.pop-sep { height: 1px; background: var(--border-subtle); margin: 6px var(--sp-1); }
.pop-title { font-weight: var(--fw-semibold); padding: 6px 10px var(--sp-2); letter-spacing: var(--ls-13); }
.pop-note { padding: var(--sp-1) 10px 6px; color: var(--text-tertiary); font-size: var(--fs-xs); letter-spacing: var(--ls-11); white-space: normal; line-height: var(--lh-base); }
.ctx-menu { min-width: 210px; }
/* A web card's context menu is ~470px tall; on a short window it ran off the bottom
   with Delete out of reach. Menus scroll inside the window instead. */
.popover { max-height: calc(100vh - 16px); overflow-y: auto; overscroll-behavior: contain; }
.connect-pop { min-width: 200px; }
.share-pop, .board-pop { width: 340px; }
.share-row { display: flex; gap: 6px; padding: 0 var(--sp-1) 6px; }
.share-row input { flex: 1; min-width: 0; height: var(--ctl-md); padding: 0 10px; background: var(--fill-subtle); border: 1px solid var(--border-default); border-radius: var(--r-sm); color: var(--text-primary); outline: 0; transition: border-color var(--dur-fast), box-shadow var(--dur-fast); }
.share-row input:focus { border-color: var(--field-focus-border); box-shadow: var(--ring-focus); }
.share-row .text-btn { flex: none; width: auto; height: var(--ctl-md); padding: 0 var(--sp-3); }
.share-access { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-1) var(--sp-2); color: var(--text-secondary); font-size: var(--fs-sm); letter-spacing: var(--ls-12); }
.share-access .pi svg { color: var(--text-tertiary); }
.share-access b { color: var(--text-primary); font-weight: var(--fw-semibold); }
.share-access .link-btn { margin-left: auto; width: auto; height: auto; padding: 0; }
.share-access .link-btn:hover { background: none; }

/* ---------- modals ---------- */
.modal-backdrop { position: fixed; inset: 0; z-index: var(--z-modal); background: var(--scrim); display: grid; place-items: center; padding: 16px; backdrop-filter: blur(3px); animation: fade-in var(--dur-base); }
.modal-backdrop.top { place-items: start center; padding-top: min(14vh, 96px); padding-top: min(14dvh, 96px); }
.modal-backdrop.closing { animation: fade-out var(--dur-base) var(--ease-in) forwards; pointer-events: none; }
.modal-backdrop.closing .modal, .modal-backdrop.closing .palette { animation: scale-out var(--dur-base) var(--ease-in) forwards; }
.modal { display: flex; flex-direction: column; width: 560px; max-width: calc(100vw - 32px); max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px); background: var(--surface-top); border: 1px solid var(--overlay-border); border-radius: var(--r-xl); box-shadow: var(--shadow-modal); overflow: hidden; animation: sheet-in var(--dur-slow) var(--ease-out-expo); }
.modal.narrow { width: 440px; }
.modal-head { display: flex; flex-shrink: 0; align-items: center; justify-content: space-between; gap: var(--sp-2); padding: var(--sp-3) var(--sp-5); font-weight: var(--fw-semibold); font-size: var(--fs-xl); letter-spacing: var(--ls-16); border-bottom: 1px solid var(--border-subtle); }
.modal-head > button { flex-shrink: 0; }
.modal-body { min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: var(--sp-4) var(--sp-5); }
.modal-foot { display: flex; flex-shrink: 0; align-items: center; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--border-subtle); }
.modal .text-btn { min-height: var(--ctl-lg); }
.modal-foot.right { justify-content: flex-end; }
.modal.confirm h3 { margin: var(--sp-1) 0 var(--sp-2); font-size: var(--fs-xl); font-weight: var(--fw-semibold); letter-spacing: var(--ls-16); }
.modal.confirm p { margin: 0; color: var(--text-secondary); line-height: var(--lh-relaxed); }
/* Native top-layer dialogs use the same frame and controls as .modal. Keep only
   their body scrollable so the title, close control and form actions stay reachable. */
.native-dialog { width: 520px; max-width: calc(100vw - 32px); max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px); padding: 0; overflow: hidden; border: 1px solid var(--overlay-border); border-radius: var(--r-dialog); background: var(--surface-top); color: var(--text-primary); box-shadow: var(--shadow-modal); font: var(--fs-md)/var(--lh-relaxed) var(--font-ui); letter-spacing: var(--ls-13); }
.native-dialog[open] { display: flex; flex-direction: column; animation: sheet-in var(--dur-slow) var(--ease-out-expo); }
.native-dialog::backdrop { background: var(--scrim); backdrop-filter: blur(4px) saturate(1.1); -webkit-backdrop-filter: blur(4px) saturate(1.1); }
.native-dialog-form { display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
.native-dialog-head { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; gap: var(--sp-3); padding: var(--sp-3) var(--sp-5); border-bottom: 1px solid var(--border-subtle); }
.native-dialog-head h2 { margin: 0; font-size: var(--fs-xl); font-weight: var(--fw-semibold); letter-spacing: var(--ls-16); line-height: var(--lh-snug); }
.native-dialog-body { min-height: 0; overflow: auto; overscroll-behavior: contain; padding: var(--sp-4) var(--sp-5); }
.native-dialog-foot { display: flex; align-items: center; justify-content: flex-end; flex-shrink: 0; gap: var(--sp-2); padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--border-subtle); }
.native-dialog button { min-height: var(--ctl-lg); padding: 0 var(--sp-3); border: 1px solid var(--border-default); border-radius: var(--r-control); background: var(--fill-subtle); color: var(--text-primary); font: inherit; font-weight: var(--fw-medium); cursor: pointer; transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast); }
.native-dialog button:not(:disabled):hover { background: var(--fill-strong); }
.native-dialog button:not(:disabled):active { background: var(--state-active); }
.native-dialog button:disabled { opacity: .5; cursor: default; }
.native-dialog :is(.collab-primary,.identity-save,.native-primary) { background: var(--accent); border-color: transparent; color: var(--text-on-accent); }
.native-dialog :is(.collab-primary,.identity-save,.native-primary):not(:disabled):hover { background: var(--accent-hover); }
.native-dialog :is(.collab-primary,.identity-save,.native-primary):not(:disabled):active { background: var(--accent-active); }
.native-dialog .native-dialog-close { display: grid; place-items: center; flex-shrink: 0; width: var(--ctl-md); height: var(--ctl-md); min-height: 0; padding: 0; border: 0; background: transparent; color: var(--text-secondary); }
.native-dialog-close svg { width: var(--icon-md); height: var(--icon-md); }
.native-dialog .native-dialog-close:hover { background: var(--state-hover); color: var(--text-primary); }
.native-dialog :is(input:not([type=color]),select) { min-width: 0; height: var(--ctl-lg); padding: 0 var(--sp-3); border: 1px solid var(--border-default); border-radius: var(--r-field); background: var(--fill-subtle); color: var(--text-primary); font: inherit; outline: 0; transition: border-color var(--dur-fast), box-shadow var(--dur-fast); }
.native-dialog input:not([type=color]) { width: 100%; }
.native-dialog :is(input:not([type=color]),select):focus { border-color: var(--field-focus-border); box-shadow: var(--ring-focus); }
.native-dialog :is(button,input[type=color]):focus-visible { outline: 2px solid var(--accent-line); outline-offset: 2px; }
.native-dialog a { color: var(--text-link); text-underline-offset: 3px; }
@media (pointer: coarse) {
  .modal .text-btn, .modal .share-made input, .native-dialog button, .native-dialog :is(input:not([type=color]),select) { min-height: var(--ctl-xl); }
  .native-dialog .native-dialog-close { width: var(--ctl-xl); height: var(--ctl-xl); }
  .native-dialog :is(input:not([type=color]),select), .modal .share-made input { font-size: var(--fs-xl); }
}
@media (prefers-reduced-transparency: reduce) { .native-dialog::backdrop { backdrop-filter: none; -webkit-backdrop-filter: none; } }
.hint { color: var(--text-tertiary); font-size: var(--fs-sm); letter-spacing: var(--ls-12); }
.field { display: block; margin-bottom: var(--sp-3); }
.field-label { display: block; font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--text-secondary); text-transform: uppercase; letter-spacing: var(--ls-caps); margin-bottom: var(--sp-2); }
.field input { width: 100%; height: var(--ctl-lg); padding: 0 var(--sp-3); background: var(--fill-subtle); border: 1px solid var(--border-default); border-radius: var(--r-md); color: var(--text-primary); outline: 0; font-size: var(--fs-md); letter-spacing: var(--ls-13); transition: border-color var(--dur-fast), box-shadow var(--dur-fast); }
.field input:focus { border-color: var(--field-focus-border); box-shadow: var(--ring-focus); }
.field input.err { border-color: var(--danger); animation: shake var(--dur-feedback) var(--ease-out); }
.url-preview { display: flex; align-items: center; gap: 10px; padding: var(--sp-2) 10px; margin: -4px 0 var(--sp-4); background: var(--fill-subtle); border: 1px solid var(--border-default); border-radius: var(--r-md); cursor: pointer; transition: border-color var(--dur-fast); animation: rise-in var(--dur-base) var(--ease-out); }
.url-preview:hover { border-color: var(--border-strong); }
.url-preview .fav { width: var(--ctl-xs); height: var(--ctl-xs); border-radius: var(--r-sm); }
.url-preview > div { flex: 1; min-width: 0; }
.url-preview b { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: var(--fw-semibold); }
.url-preview small { display: block; color: var(--text-tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: var(--fs-xs); letter-spacing: var(--ls-11); }
.url-preview-cta { font-size: var(--fs-xs); letter-spacing: var(--ls-11); color: var(--text-tertiary); font-weight: var(--fw-semibold); }
.site-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--sp-2); margin-bottom: 14px; }
/* 1fr has a min-content floor, so on a phone the five tiles overflowed the dialog
   and the last column was cut in half; minmax(0) lets them shrink, the label clips. */
.site > span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.modal-foot .text-btn { flex: none; white-space: nowrap; }

/* Sharing stays compact; one export format is shown only after disclosure. */
.share-modal { width: 480px; }
.share-what { margin: 0 0 var(--sp-3); color: var(--text-secondary); font-size: var(--fs-sm); letter-spacing: var(--ls-12); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.share-item { display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; align-items: center; gap: var(--sp-3); padding: 10px 0; border-top: 1px solid var(--border-subtle); }
.share-item:first-child { border-top: 0; }
.share-item:has(.share-made) { grid-template-columns: 20px minmax(0, 1fr); }
.share-item:has([data-share="board-copy"]) { grid-template-columns: 20px minmax(0, 1fr); }
.share-item:has([data-share="board-copy"]) > .share-acts { grid-column: 2; flex-wrap: wrap; margin-top: var(--sp-1); }
.share-acts:empty { display: none; }
.share-ic { display: grid; place-items: center; color: var(--text-secondary); }
.share-ic svg { width: var(--icon-md); height: var(--icon-md); }
.share-t { font-weight: var(--fw-medium); color: var(--text-primary); }
.share-d { margin-top: 2px; font-size: var(--fs-sm); letter-spacing: var(--ls-12); line-height: var(--lh-snug); color: var(--text-secondary); }
.share-acts { display: flex; gap: 6px; }
.share-acts .text-btn, .share-made .text-btn { height: var(--ctl-lg); padding: 0 var(--sp-3); font-size: var(--fs-sm); letter-spacing: var(--ls-12); white-space: nowrap; }
.share-made { display: flex; gap: 6px; margin: 2px 0 6px; }
.share-made input { flex: 1; min-width: 0; height: var(--ctl-lg); padding: 0 var(--sp-3); border: 1px solid var(--border-default); border-radius: var(--r-md); background: var(--fill-subtle); color: var(--text-primary); font: inherit; font-size: var(--fs-sm); }
.share-manage { color: var(--text-secondary); }
.share-error { margin: var(--sp-2) 0; color: var(--danger-text); font-size: var(--fs-sm); line-height: var(--lh-snug); }
.share-export { margin-top: var(--sp-3); border-top: 1px solid var(--border-subtle); }
.share-export summary { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); min-height: var(--ctl-xl); list-style: none; cursor: pointer; color: var(--text-secondary); border-radius: var(--r-sm); }
.share-export summary::-webkit-details-marker { display: none; }
.share-export summary:hover { color: var(--text-primary); }
.share-chevron { display: grid; place-items: center; }
.share-chevron svg { width: var(--icon-md); height: var(--icon-md); transition: transform var(--dur-fast); }
.share-export[open] .share-chevron svg { transform: rotate(180deg); }
.share-export-body { padding: var(--sp-1) 0 var(--sp-3); }
.share-public-body { padding-bottom: var(--sp-3); }
.share-export-body > label { display: block; margin-bottom: var(--sp-2); color: var(--text-secondary); font-size: var(--fs-sm); }
.share-export-body > select { width: 100%; height: var(--ctl-lg); padding: 0 var(--sp-3); border: 1px solid var(--border-default); border-radius: var(--r-md); background: var(--fill-subtle); color: var(--text-primary); }
.share-export-output .share-d { margin: var(--sp-2) 0 var(--sp-3); }
.share-export-output .share-acts { justify-content: flex-end; }
.share-body .share-identity { gap: var(--sp-2); padding: var(--sp-3) 0 0; margin: 0; border-top: 1px solid var(--border-subtle); border-bottom: 0; }
.share-body .share-identity .identity-face { width: 28px; height: 28px; font-size: var(--fs-sm); }
.share-body .share-identity small { margin: 0 0 2px; font-size: var(--fs-xs); }
.share-body .share-identity strong { font-size: var(--fs-sm); font-weight: var(--fw-medium); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.share-body .share-identity [data-identity-edit] { padding: 0 var(--sp-2); background: transparent; color: var(--text-secondary); }
.share-body .share-identity [data-identity-edit]:hover { background: var(--state-hover); color: var(--text-primary); }
@media (max-width: 520px) { .share-main .share-item { grid-template-columns: 20px minmax(0, 1fr); } .share-main .share-acts { grid-column: 2; } }
@media (pointer: coarse) { .share-export-body > select { height: var(--ctl-xl); font-size: var(--fs-xl); } }
/* Cite (app.js openCite): the style chosen beside what it gives. */
.cite-modal { width: 600px; }
.cite-styles { width: max-content; max-width: 100%; margin-bottom: var(--sp-3); overflow-x: auto; }
.cite-out { max-height: min(46vh, 360px); overflow: auto; padding: var(--sp-3); border: 1px solid var(--border-subtle); border-radius: var(--r-md); background: var(--fill-subtle); color: var(--text-primary); font-size: var(--fs-sm); line-height: 1.55; user-select: text; -webkit-user-select: text; }
.cite-out p { margin: 0 0 var(--sp-2); padding-left: 2em; text-indent: -2em; } /* a hanging indent, as reference lists are set */
.cite-out p:last-child { margin-bottom: 0; }
.cite-out pre { margin: 0; white-space: pre-wrap; word-break: break-word; font: inherit; }
.cite-out.mono pre { font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: 0; }
.cite-none { color: var(--text-tertiary); }
.site { display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); padding: var(--sp-3) 6px 10px; border-radius: var(--r-lg); background: var(--fill-subtle); border: 1px solid var(--border-subtle); color: var(--text-secondary); font-size: var(--fs-sm); letter-spacing: var(--ls-12); transition: background var(--dur-fast), transform var(--dur-fast) var(--ease-out), border-color var(--dur-fast); }
.site:hover { background: var(--state-hover); color: var(--text-primary); border-color: var(--border-default); }
.site img { width: 32px; height: 32px; border-radius: var(--r-md); }
.recent-list { display: flex; flex-direction: column; gap: var(--sp-1); }
.recent { display: grid; grid-template-columns: 18px 1fr auto; align-items: center; gap: 10px; padding: 7px 10px; border-radius: var(--r-md); color: var(--text-primary); text-align: left; transition: background var(--dur-fast); }
.recent:hover { background: var(--state-hover); }
.recent img { width: var(--icon-md); height: var(--icon-md); border-radius: var(--r-xs); }
.recent-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.recent-host { color: var(--text-tertiary); font-size: var(--fs-xs); letter-spacing: var(--ls-11); }
.shortcuts { display: grid; grid-template-columns: 1fr 1fr; gap: 9px var(--sp-5); color: var(--text-secondary); }
.shortcuts kbd { margin-right: 6px; color: var(--text-primary); }
.sc-group { grid-column: 1 / -1; font-size: var(--fs-xs); font-weight: var(--fw-semibold); text-transform: uppercase; letter-spacing: var(--ls-caps); color: var(--text-tertiary); margin-top: 6px; }
.sc-group:first-child { margin-top: 0; }
/* Two columns of "key  what it does" need the regular width: at 440px a third of the
   rows wrapped their words under the key ("Paste cards, a link, or / text"). */
.modal:has(> .shortcuts) { width: 560px; }

/* ---------- research search palette ---------- */
.palette { display: flex; flex-direction: column; width: 680px; max-width: calc(100vw - 32px); max-height: calc(100vh - min(14vh, 96px) - 16px); max-height: calc(100dvh - min(14dvh, 96px) - 16px); background: var(--surface-top); border: 1px solid var(--overlay-border); border-radius: var(--r-xl); box-shadow: var(--shadow-modal); overflow: hidden; animation: sheet-in var(--dur-slow) var(--ease-out-expo); }
.palette-input { display: flex; flex-shrink: 0; align-items: center; gap: 10px; padding: var(--sp-3) var(--sp-5); border-bottom: 1px solid var(--border-subtle); }
.palette-input .pi svg { color: var(--text-secondary); }
.palette-input input { flex: 1; min-width: 0; background: none; border: 0; outline: 0; color: var(--text-primary); font-size: var(--fs-xl); letter-spacing: var(--ls-16); }
.palette-input .icon-btn { flex-shrink: 0; }
.palette-input .search-dismiss { width: auto; min-width: 36px; }
.palette-input .search-dismiss kbd { margin: 0; }
.palette-tools { display: flex; flex-shrink: 0; align-items: center; gap: 8px; padding: 8px 15px; border-bottom: 1px solid var(--border-subtle); }
.palette-tools select { min-width: 0; max-width: 170px; height: var(--ctl-sm); padding: 0 7px; border: 1px solid var(--border-subtle); border-radius: var(--r-sm); background: var(--surface-top); color: var(--text-secondary); font: inherit; font-size: var(--fs-sm); letter-spacing: var(--ls-12); cursor: pointer; }
.search-status { margin-left: auto; font-size: var(--fs-xs); color: var(--text-secondary); white-space: nowrap; font-variant-numeric: tabular-nums; }
.palette-list { min-height: 0; max-height: 400px; overflow: auto; overscroll-behavior: contain; padding: 5px; scrollbar-gutter: stable; }
.pl-item { display: grid; grid-template-columns: 18px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--r-md); cursor: pointer; transition: background var(--dur-instant); }
.pl-item.on { background: var(--state-selected); }
.pl-item .fav { width: var(--icon-md); height: var(--icon-md); }
.pl-item .ti svg { width: var(--icon-md); height: var(--icon-md); color: var(--text-secondary); }
.pl-main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.pl-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: var(--fs-md); font-weight: var(--fw-medium); letter-spacing: var(--ls-13); }
.pl-sub { color: var(--text-secondary); font-size: var(--fs-xs); letter-spacing: var(--ls-11); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pl-excerpt { color: var(--text-secondary); font-size: var(--fs-sm); line-height: 1.45; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pl-item mark { color: inherit; background: transparent; font-weight: var(--fw-semibold); text-decoration: underline; text-decoration-color: var(--accent); text-underline-offset: 3px; }
.pl-go { color: var(--text-secondary); opacity: 0; font-size: var(--fs-xs); }
.pl-item.on .pl-go { opacity: 1; }
.pl-empty { display: flex; flex-direction: column; gap: 6px; padding: 22px 16px; text-align: center; color: var(--text-secondary); font-size: var(--fs-sm); }
.pl-empty strong { color: var(--text-primary); font-size: var(--fs-md); font-weight: var(--fw-medium); }
.pl-group { padding: 9px 12px 5px; font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--text-secondary); }
.palette-foot { display: flex; flex-shrink: 0; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); padding: 10px var(--sp-5); border-top: 1px solid var(--border-subtle); color: var(--text-secondary); font-size: var(--fs-xs); letter-spacing: var(--ls-11); }
.palette-foot kbd { margin-right: var(--sp-1); }
.palette-command-hint { margin-left: auto; }
@media (max-width: 480px) {
  .palette-input { padding: 10px 12px; gap: 8px; }
  .palette-input input { font-size: 16px; }
  .palette-tools { gap: 6px; padding: 8px 10px; flex-wrap: wrap; }
  .palette-tools select { max-width: 145px; }
  .palette-foot { padding: 10px 12px; gap: 8px 12px; }
  .palette-command-hint { margin-left: 0; }
}
@media (pointer: coarse) { .palette-tools select { min-height: 44px; } .palette-input .icon-btn { min-width: 44px; min-height: 44px; } .pl-item { min-height: 44px; } }

/* ---------- feedback ---------- */
.toast { display: flex; align-items: center; gap: var(--sp-3); position: fixed; left: 50%; bottom: calc(64px + env(safe-area-inset-bottom)); width: max-content; max-width: calc(100vw - 32px); transform: translateX(-50%); z-index: var(--z-toast); background: var(--surface-inverted); color: var(--text-on-inverted); padding: 9px 14px; border-radius: var(--r-md); font-weight: var(--fw-medium); box-shadow: var(--shadow-lg); }
@keyframes toast-in { from { opacity: 0; translate: 0 var(--motion-offset); scale: var(--motion-scale); } }
.toast.pop { animation: toast-in var(--dur-slow) var(--ease-out-expo); }
.toast.hide { animation: toast-out var(--dur-base) var(--ease-in) forwards; }
.toast-act { margin: -3px -6px -3px 0; height: var(--ctl-xs); padding: 0 var(--sp-2); border-radius: var(--r-sm); font-weight: var(--fw-semibold); color: var(--text-on-inverted); background: color-mix(in oklch, var(--text-on-inverted) 14%, transparent); transition: background var(--dur-fast); }
.toast-act:hover { background: color-mix(in oklch, var(--text-on-inverted) 24%, transparent); }
.toast-act:focus-visible { outline: 2px solid var(--text-on-inverted); outline-offset: 1px; }
/* A source row's tip is its URL, which can be long; the tip is a label, not a
   paragraph, so it caps and ellipsises rather than stretching across the window. */
.tip { max-width: min(460px, 88vw); }
.tip-text { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.tip { position: fixed; z-index: var(--z-tip); pointer-events: none; display: flex; align-items: center; gap: var(--sp-2); background: var(--surface-inverted); color: var(--text-on-inverted); padding: 5px var(--sp-2); border-radius: var(--r-sm); font-size: var(--fs-xs); letter-spacing: var(--ls-11); box-shadow: var(--shadow-md); white-space: nowrap; animation: tip-in var(--dur-fast) var(--ease-out); }
.tip kbd { height: 16px; min-width: 16px; padding: 0 4px; background: color-mix(in oklch, var(--text-on-inverted) 13%, transparent); border-color: color-mix(in oklch, var(--text-on-inverted) 24%, transparent); color: var(--text-on-inverted); }
/* Source details remain usable while crossing the small gap from their trigger. */
.source-preview { position: fixed; z-index: 1100; width: min(320px, calc(100vw - 24px)); max-height: calc(100dvh - 24px); display: flex; flex-direction: column; padding: 8px; border: 1px solid var(--overlay-border); border-radius: var(--r-md); background: var(--surface-raised); color: var(--text-primary); box-shadow: var(--shadow-lg); font-size: var(--fs-sm); line-height: 1.45; animation: source-preview-in var(--dur-base) var(--ease-out); transition: opacity var(--dur-fast), translate var(--dur-fast) var(--ease-out); }
.source-preview[data-side="above"] { --source-rise: -4px; }
@keyframes source-preview-in { from { opacity: 0; translate: 0 var(--source-rise, 4px); } }
.source-preview.closing { animation: none; opacity: 0; translate: 0 var(--source-rise, 4px); pointer-events: none; }
.source-preview-head { display: flex; align-items: center; justify-content: space-between; padding-left: 4px; color: var(--text-secondary); font-size: var(--fs-xs); }
.source-preview-close { width: 24px; height: 24px; display: grid; place-items: center; border-radius: var(--r-sm); color: var(--text-secondary); }
.source-preview-close svg { width: 14px; height: 14px; }
.source-preview-body { min-height: 0; overflow: auto; overscroll-behavior: contain; max-height: min(320px, calc(100dvh - 124px)); padding: 6px 4px 12px; user-select: text; }
.source-preview-title { font-weight: var(--fw-medium); overflow-wrap: anywhere; }
.source-preview-meta { margin-top: 6px; color: var(--text-secondary); font-size: var(--fs-xs); overflow-wrap: anywhere; }
.source-preview-url { margin-top: 6px; color: var(--text-secondary); font-size: var(--fs-xs); overflow-wrap: anywhere; }
.source-preview-actions { display: flex; align-items: center; gap: 6px; padding-top: 8px; border-top: 1px solid var(--border-subtle); }
.source-preview-actions button { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; height: 28px; border-radius: var(--r-sm); color: var(--text-secondary); font-size: var(--fs-xs); transition: background var(--dur-fast), color var(--dur-fast); }
.source-preview-actions svg { width: 14px; height: 14px; }
.source-preview-actions [data-source-action="open"] { background: var(--action-tonal-bg); color: var(--action-tonal-fg); }
.source-preview button:hover { background: var(--state-hover); color: var(--text-primary); }
.source-preview-actions .copied { color: var(--action-tonal-fg); background: var(--action-tonal-bg); }
.source-preview button:active { background: var(--state-active); }
.source-preview button:focus-visible { outline-offset: 1px; }
@media (pointer: coarse) { .source-preview-close, .source-preview-actions button { min-height: 44px; } .source-preview-close { width: 44px; } .source-preview-body { max-height: min(320px, calc(100dvh - 164px)); } }

/* ---------- notifications ---------- */
.has-badge { position: relative; }
.badge-dot { position: absolute; top: 4px; right: 4px; width: 7px; height: 7px; border-radius: 50%; background: var(--danger); box-shadow: 0 0 0 2px var(--surface-chrome); }
.notify-pop { width: 360px; padding: 6px; }
.pop-title-row { display: flex; align-items: center; justify-content: space-between; padding-right: 10px; }
.pop-title-row .link-btn { width: auto; height: auto; padding: 0; font-size: var(--fs-xs); letter-spacing: var(--ls-11); }
.pop-title-row .link-btn:hover { background: none; }
.notify-list { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow: auto; }
.note-item { display: grid; grid-template-columns: 28px 1fr auto; gap: 10px; align-items: start; padding: 8px 10px; border-radius: var(--r-md); cursor: default; transition: background var(--dur-fast); }
.note-item:hover { background: var(--state-hover); }
.note-icon { width: var(--row-sm); height: var(--row-sm); border-radius: var(--r-sm); display: grid; place-items: center; background: var(--fill-subtle); color: var(--text-secondary); }
.note-icon svg { width: var(--icon-md); height: var(--icon-md); }
.note-icon.board { background: var(--fill-subtle); color: var(--text-secondary); }
.note-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; white-space: normal; }
.note-body b { font-weight: var(--fw-semibold); font-size: var(--fs-md); letter-spacing: var(--ls-13); line-height: var(--lh-snug); }
.note-body span { color: var(--text-secondary); font-size: var(--fs-sm); letter-spacing: var(--ls-12); line-height: var(--lh-snug); }
.note-time { font-size: var(--fs-xs); letter-spacing: var(--ls-11); color: var(--text-tertiary); padding-top: 2px; white-space: nowrap; }
.note-item.unread b { color: var(--text-primary); }
.note-item:not(.unread) b, .note-item:not(.unread) .note-body span { color: var(--text-tertiary); }
.note-item.unread .note-time::before { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); margin-right: 6px; vertical-align: middle; }

/* ============================================================
   Card interaction spec — §4.4 sizing, §4.1 live-page mode,
   §4.3 scroll boundary, §4.7 chrome, §4.8 access & touch.

   The rule underneath all of this: content lives in the world and
   scales with it; the chrome you use to *operate* a card lives on
   the screen and does not. `--px` is one screen pixel expressed in
   world units, capped so the compensation stops at the LOD threshold
   in one direction and at 400% in the other.

   applyTransform() writes the capped reciprocal `--ui` directly rather
   than the raw zoom. --ui is inherited by everything in the world, so
   every write re-resolves the computed style of the whole board; writing
   the *capped* value means that outside 42%-400% — which is exactly
   where the most cards are on screen at once — a zoom changes nothing
   and costs nothing.
   ============================================================ */
.world { --ui: 1; --px: calc(1px * var(--ui));
  /* --px deliberately stops compensating below ~42% zoom so card furniture
     cannot outgrow its card. A frame's edge has no such problem and must not
     disappear exactly when you zoom out to see regions, so it gets the
     uncapped reciprocal and stays 1.5 screen px at every zoom. */
  /* A region wants less corner than a card, not more. */
  --frame-r: calc(3 * var(--px));
  /* A card's corner and its depth are chrome, not geometry the user set,
     so both are measured in screen pixels like the rest of the card's
     furniture. Left in world units a 10px corner renders 0.8px at 8%
     zoom and 40px at 400%, around a title bar that stayed 32px. */
  --card-r: calc(7 * var(--px));
  --card-shadow: 0 0 0 transparent;
  --card-shadow-hover: var(--card-shadow);
  --card-shadow-drag: 0 calc(4 * var(--px)) calc(12 * var(--px)) var(--shadow-direct);
  --card-ring:       inset 0 0 0 calc(1 * var(--px)) var(--border-default);
  --card-ring-hover: var(--card-ring);
  --card-top:        inset 0 0 0 transparent;
  --card-elev:       var(--card-shadow), var(--card-ring), var(--card-top);
  --card-elev-hover: var(--card-shadow-hover), var(--card-ring-hover), var(--card-top);
  --card-elev-drag:  var(--card-shadow-drag), var(--card-ring-hover), var(--card-top);
}

/* Resolve edge tokens on the object, where each note supplies its color. */
.card {
  --card-ring-hover: var(--card-ring);
  --card-elev: var(--card-shadow), var(--card-ring), var(--card-top);
  --card-elev-hover: var(--card-elev);
  --card-elev-drag: var(--card-shadow-drag), var(--card-ring), var(--card-top);
}

/* ---------- card chrome, screen-constant ---------- */
.card { outline-width: calc(1.5 * var(--px)); border-radius: var(--card-r); box-shadow: var(--card-elev); }
.card:not(.frame):hover { box-shadow: var(--card-elev-hover); }
/* The halo has to be translucent: it sits on the canvas, not on the card.
   (--accent-soft is an opaque ramp step and painted a solid indigo slab.) */
/* A thin frame set one pixel off the card reads as precision; a thick one reads as
   loud. That 1px of ground between card and frame is what makes it look measured —
   and it stops the frame from covering the card's own hairline. The soft halo is
   gone from plain selection: it made the object look fuzzy at exactly the moment
   the user wants to see its edges. Lifting and live mode still get a glow, because
   there the glow is saying something the frame cannot. */
.card.selected { outline-width: calc(1 * var(--px)); outline-offset: calc(1 * var(--px)); box-shadow: var(--card-elev); }
.card.selected:hover { box-shadow: var(--card-elev-hover); }
.card.lifted { box-shadow: var(--card-elev-drag); }
.card.link-target { outline-width: calc(2 * var(--px)); box-shadow: var(--card-elev); }
.card.frame { border-radius: var(--frame-r); }
.chb:hover, .chb:active { border-radius: calc(5 * var(--px)); }
.card-head { height: calc(32 * var(--px)); gap: calc(8 * var(--px)); padding: 0 calc(6 * var(--px)) 0 calc(10 * var(--px)); font-size: calc(12 * var(--px)); letter-spacing: var(--ls-12); background-size: 100% calc(1 * var(--px)); }
.card-head .fav, .card-head .ti svg { width: calc(16 * var(--px)); height: calc(16 * var(--px)); }
.card-head-actions { gap: calc(2 * var(--px)); }
/* The head actions float over the end of the title instead of holding a column.
   Invisible at rest, they still took ~95px of a page's title bar, so at 46% a card
   said "Visit Canada …" with empty space beside it. Now the title runs the full
   width at rest; when the actions show (hover, selection, peek source) the end of
   the title fades out beneath them — the outline rows' technique, so nothing shifts
   under the pointer. A live card and a touch screen keep the column: there the
   actions are always showing and share the bar with LIVE and Done. */
.card-head { position: relative; }
.card { --head-act-w: calc(50 * var(--px)); }
.card.web { --head-act-w: calc(128 * var(--px)); } /* five direct actions */
.card-head-actions { position: absolute; right: calc(6 * var(--px)); top: 50%; translate: 0 -50%; }
.card.web:hover > .card-head > .card-title, .card.web.selected > .card-head > .card-title, .card.web.peek-source > .card-head > .card-title,
.card.subcanvas:hover > .card-head > .card-title, .card.subcanvas.selected > .card-head > .card-title {
  text-overflow: clip;
  mask-image: linear-gradient(90deg, #000 calc(100% - var(--head-act-w) - 18 * var(--px)), transparent calc(100% - var(--head-act-w) - 4 * var(--px)));
}
.card.web.interacting .card-head-actions { position: static; translate: none; }
/* A rename field stops short of the floating actions so what is typed stays in view. */
.card-head .rename-input { margin-right: var(--head-act-w); }
.card.web.interacting .card-title { mask-image: none; text-overflow: ellipsis; }
.chb { width: calc(24 * var(--px)); height: calc(24 * var(--px)); border-radius: calc(5 * var(--px)); }
.chb svg { width: calc(16 * var(--px)); height: calc(16 * var(--px)); }
.frame-label { top: calc(-28 * var(--px)); height: calc(28 * var(--px)); gap: calc(6 * var(--px)); padding: 0 calc(8 * var(--px)) 0 calc(6 * var(--px)); font-size: calc(12 * var(--px)); letter-spacing: var(--ls-12); border-radius: calc(5 * var(--px)); }
.frame-label .ti svg { width: calc(14 * var(--px)); height: calc(14 * var(--px)); }
.frame-count { font-size: calc(11 * var(--px)); letter-spacing: var(--ls-11); }
.lod-cover { inset: calc(32 * var(--px)) 0 0 0; }
/* Nothing else here for LOD: below the threshold the label owns the whole card
   and is sized from the card (see the LOD block above), not from the screen.
   It is content, and content scales with the world. */
.cards.lod .frame-label { top: calc(-28 * var(--px)); font-size: calc(12 * var(--px)); letter-spacing: var(--ls-12); padding: 0 calc(8 * var(--px)) 0 calc(6 * var(--px)); }
.cards.lod .frame-label .ti svg { width: calc(14 * var(--px)); height: calc(14 * var(--px)); }
.cards.lod .rz { display: none; }
.app.present .rz { display: none; }
/* clicking a connector is a frequent action: keep its hit area 16 screen px wide */
.links .hit { stroke-width: 16; vector-effect: non-scaling-stroke; }

/* ---------- eight resize handles (§4.4) ---------- */
.resize { display: none; }
.rz { position: absolute; z-index: 4; }
.rz-n, .rz-s { left: calc(14 * var(--px)); right: calc(14 * var(--px)); height: calc(8 * var(--px)); cursor: ns-resize; }
.rz-e, .rz-w { top: calc(14 * var(--px)); bottom: calc(14 * var(--px)); width: calc(8 * var(--px)); cursor: ew-resize; }
.rz-n { top: 0; } .rz-s { bottom: 0; } .rz-w { left: 0; } .rz-e { right: 0; }
.rz-nw, .rz-ne, .rz-se, .rz-sw { width: calc(14 * var(--px)); height: calc(14 * var(--px)); }
.rz-nw { top: 0; left: 0; cursor: nwse-resize; }
.rz-ne { top: 0; right: 0; cursor: nesw-resize; }
.rz-se { bottom: 0; right: 0; cursor: nwse-resize; }
.rz-sw { bottom: 0; left: 0; cursor: nesw-resize; }
/* .rz elements are hit zones only now — they carry no mark. The visible handle is
   drawn on the selection layer (.rz-handle) so it can straddle the card's edge;
   drawn in here it had to hide inside the corner, where it read as a printer's
   crop mark sitting on top of the page. */

/* ---------- live-page mode (§4.1) ---------- */
/* Not interacting: the card is an object. The page underneath is inert and the whole
   surface is a drag grip, so a stray click can never fire a link. */
.card.web:not(.interacting) .card-body { cursor: default; user-select: none; }
.card.web:not(.interacting) .card-body a { cursor: default; }
.card.web.interacting .card-body { cursor: auto; user-select: text; }
/* Live is one state, drawn on the card and nowhere else (NN/g on modes: redundant,
   strong, permanently visible signals, where attention already is):
   1. the frame — a 2px accent ring set 2px off the card, over a soft accent glow,
      on the lifted (drag-depth) shadow, so the page sits above the board;
   2. the title bar — tinted toward the accent, with "Live" and a Done button;
   3. the board — everything else steps back to 40%, connectors and labels included,
      except the connectors that touch this card. */
.card.interacting {
  outline-width: calc(2 * var(--px)); outline-offset: calc(2 * var(--px)); outline-color: var(--accent); outline-style: solid;
  box-shadow: var(--card-elev);
}
/* The title bar is ours; the page below it is not. Tinting the bar says "this window
   is live" without putting a sticker on top of someone's content. */
/* Only web cards ever go live, and a web card's head is painted from the PAGE
   palette by .card.web.selected — so the tint has to answer at that same
   specificity and mix into the page's own head colour, or it silently loses. */
.card.web.interacting .card-head { --head-fill: color-mix(in oklch, var(--accent) 16%, var(--card-chrome-page-selected)); }
/* The label is quiet (a dot and a word in accent ink); the one filled thing in the bar
   is the way out. One solid per view, and here the solid is "Done". */
.live-badge {
  display: none; flex: none; align-items: center; gap: calc(5 * var(--px));
  color: var(--accent); font-size: calc(10 * var(--px)); font-weight: var(--fw-bold); letter-spacing: var(--ls-caps); text-transform: uppercase;
  pointer-events: none; animation: fade-in var(--dur-base) var(--ease-out);
}
.live-badge::before { content: ""; width: calc(6 * var(--px)); height: calc(6 * var(--px)); border-radius: 50%; background: currentColor; }
.card.interacting .live-badge { display: flex; }
.live-done {
  display: none; flex: none; align-items: center; height: calc(24 * var(--px)); padding: 0 calc(10 * var(--px)); margin-left: calc(4 * var(--px)); /* 24: WCAG 2.5.8 */
  border-radius: calc(5 * var(--px)); background: var(--accent); color: var(--text-on-accent);
  font-size: calc(11 * var(--px)); font-weight: var(--fw-semibold); letter-spacing: var(--ls-11);
  transition: background var(--dur-fast);
}
.live-done:hover { background: var(--accent-hover); }
.card.interacting .live-done { display: inline-flex; animation: fade-in var(--dur-base) var(--ease-out); }
/* Before going live: an invitation inside the selected page, where the pointer is.
   It waits a beat so a click-and-drag never flashes it, and it retires once learned. */
.use-hint {
  position: absolute; left: 50%; bottom: calc(14 * var(--px)); translate: -50% 0; z-index: 3;
  display: flex; align-items: center; gap: calc(6 * var(--px)); height: calc(26 * var(--px)); padding: 0 calc(6 * var(--px)) 0 calc(11 * var(--px));
  border-radius: var(--r-full); background: var(--on-page-scrim); color: var(--on-page-fg);
  font-size: calc(12 * var(--px)); font-weight: var(--fw-medium); letter-spacing: var(--ls-12); white-space: nowrap;
  pointer-events: none; opacity: 0; transition: opacity var(--dur-base) var(--ease-out);
}
/* Only the selected page can show the hint, so only it pays for the blur. A
   backdrop-filter makes a compositor layer even at opacity 0, and promotes everything
   drawn over it: on every page card that was ~1 + 4 layers each (202 → 172 on a
   76-card board). */
.card.web.selected .use-hint { -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
.use-hint kbd { height: calc(16 * var(--px)); min-width: calc(16 * var(--px)); font-size: calc(10 * var(--px)); padding: 0 calc(5 * var(--px)); border-radius: calc(4 * var(--px)); color: var(--on-page-fg); background: color-mix(in oklch, var(--on-page-fg) 16%, transparent); border-color: color-mix(in oklch, var(--on-page-fg) 26%, transparent); }
.card.web.selected:not(.interacting):hover .use-hint, .card.web.selected:not(.interacting):focus-visible .use-hint { opacity: 1; transition-delay: 350ms; }
@media (hover: none) { .card.web.selected:not(.interacting) .use-hint { opacity: 1; transition-delay: 350ms; } }
.page-scroll:focus { outline: none; }
.page-scroll:focus-visible { outline: calc(2 * var(--px, 1px)) solid var(--accent-line); outline-offset: calc(-2 * var(--px, 1px)); }
.page-scroll a[role="link"]:focus-visible { outline: calc(2 * var(--px, 1px)) solid var(--accent-line); outline-offset: calc(2 * var(--px, 1px)); border-radius: calc(2 * var(--px, 1px)); }
.app.live-learned .use-hint, .card.no-live .use-hint, .cards.lod .use-hint, .viewport.dragging .use-hint, .app.present .use-hint, .card.loading .use-hint { display: none; }
.card { transition: outline-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-base) var(--ease-out), scale var(--dur-base) var(--ease-out), opacity var(--dur-base) var(--ease-out); }
.app.interacting .card:not(.interacting),
.app.interacting .links .link:not(.related),
.app.interacting .cards:not(.lod) ~ .link-labels { opacity: 0.4; }
.app.interacting .canvas-grid { opacity: 0.45; }
.links .link { transition: color var(--dur-fast), opacity var(--dur-base) var(--ease-out); }

/* ---------- scroll boundary glow (§4.3) ---------- */
.card.edge-top::after, .card.edge-bottom::after {
  content: ""; position: absolute; left: 0; right: 0; height: calc(2 * var(--px)); z-index: 6;
  background: var(--accent); box-shadow: 0 0 calc(10 * var(--px)) var(--accent);
  pointer-events: none; animation: edge-glow var(--dur-lazy) var(--ease-in-out) both;
}
.card.edge-top::after { top: calc(32 * var(--px)); }
.card.edge-bottom::after { bottom: 0; }
@keyframes edge-glow { 0% { opacity: 0; } 35% { opacity: 1; } 100% { opacity: 0; } }

/* ---------- back-to-the-board beacon (§4.3) ---------- */
.offscreen-beacon {
  position: absolute; z-index: var(--z-toolbar); transform: translate(-50%, -50%);
  display: flex; align-items: center; gap: var(--sp-2); height: 32px; padding: 0 10px 0 8px;
  border-radius: var(--r-full); background: var(--surface-overlay); border: 1px solid var(--overlay-border);
  box-shadow: var(--shadow-md); color: var(--text-primary); font-size: var(--fs-sm); letter-spacing: var(--ls-12); white-space: nowrap;
  animation: rise-in var(--dur-base) var(--ease-out);
}
.offscreen-beacon:hover { background: var(--state-hover); }
.beacon-arrow { display: grid; place-items: center; color: var(--accent); }
.beacon-arrow svg { width: var(--icon-md); height: var(--icon-md); }
.app.present .offscreen-beacon, .app.expanded .offscreen-beacon { display: none; }

/* ---------- Side Peek footnote (§4.7) ---------- */
.peek-foot { color: var(--text-secondary); }
.peek-src { display: flex; align-items: center; gap: 6px; min-width: 0; max-width: 240px; height: var(--ctl-xs); padding: 0 8px; border-radius: var(--r-sm); color: var(--text-secondary); font-size: var(--fs-xs); letter-spacing: var(--ls-11); transition: background var(--dur-fast), color var(--dur-fast); }
.peek-src:hover { background: var(--state-hover); color: var(--text-primary); }
.peek-src img { width: 14px; height: 14px; flex: none; }
.peek-src span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.peek-foot .peek-pin-btn { margin-left: auto; }
.peek-pin-btn.on-canvas { background: none; color: var(--text-secondary); }

/* ---------- keyboard reach (§4.5) ---------- */
.viewport:focus { outline: none; }
/* No focus ring on the canvas itself. It is the application's default focus
   target and is focused almost all the time, so a full-viewport accent frame
   is permanent noise that says nothing — there is nothing on screen for it to
   distinguish the canvas from. Keyboard focus stays visible where it carries
   information: on the cards, via .card:focus-visible below. */
.viewport:focus-visible { outline: none; }
.card:focus { outline-color: var(--accent-line); }
/* one visual for focus and selection, plus an offset ring when the keyboard put it there */
.card:focus-visible { outline-color: var(--accent-line); outline-offset: calc(2 * var(--px)); box-shadow: var(--card-elev); }
.ctx-menu button:focus-visible { outline: 2px solid var(--accent-line); outline-offset: -2px; background: var(--state-hover); }
/* Every menu is driven like the context menu now: focus is the highlight, and the
   keyboard's focus adds a hairline so it cannot be mistaken for a stray hover. */
.popover[role="menu"] button:focus { outline: none; background: var(--state-hover); }
.popover[role="menu"] button:focus-visible { outline: 2px solid var(--accent-line); outline-offset: -2px; }
.ol-item:focus { outline: none; }
.ol-item:focus-visible { outline: 2px solid var(--accent-line); outline-offset: -2px; }

/* ---------- contrast: 11px meta text is never tertiary (§4.8, WCAG 1.4.3) ---------- */
.outline-foot, .note-time, .recent-host, .url-preview small, .pl-sub, .palette-foot, .pop-note, .fmt-hint { color: var(--text-secondary); }

/* ---------- touch (§4.8) ---------- */
@media (pointer: coarse) {
  .viewport { touch-action: none; }
  .card-head { height: calc(44 * var(--px)); }
  .chb { width: calc(40 * var(--px)); height: calc(40 * var(--px)); }
  .chb svg { width: calc(20 * var(--px)); height: calc(20 * var(--px)); }
  .card-head-actions { opacity: 1; position: static; translate: none; }
  .card .card-title, .card:hover .card-title, .card.selected .card-title, .card.peek-source .card-title { mask-image: none; text-overflow: ellipsis; }
  .lod-cover { inset: calc(44 * var(--px)) 0 0 0; }
  .card.edge-top::after { top: calc(44 * var(--px)); }
  .live-done { height: calc(36 * var(--px)); padding: 0 calc(14 * var(--px)); font-size: calc(13 * var(--px)); }
  .rz-nw, .rz-ne, .rz-se, .rz-sw { width: calc(24 * var(--px)); height: calc(24 * var(--px)); }
  .rz-n, .rz-s { height: calc(14 * var(--px)); left: calc(24 * var(--px)); right: calc(24 * var(--px)); }
  .rz-e, .rz-w { width: calc(14 * var(--px)); top: calc(24 * var(--px)); bottom: calc(24 * var(--px)); }
  .rz-handle { width: 13px; height: 13px; margin: -6.5px 0 0 -6.5px; transform-origin: 6.5px 6.5px; border-radius: 3px; }
}

/* Touch has no hover: without this the last-tapped card keeps its lift. */
@media (hover: none) {
  .card:not(.frame):hover { box-shadow: var(--card-elev); }
  .card.frame:hover { background: var(--frame-fill); }
  .card.selected:hover { box-shadow: var(--card-elev); }
}

/* ============================================================
   质感规范 §4.3 / §4.6 / §4.7 —— 光学、材质、微观基元
   （见「视觉质感规范」文档；每条都对应一处实测差距）
   ============================================================ */

/* ---------- §4.3 图标描边：渲染宽度恒定 ----------
   整套图标（Lucide）都是 stroke 2 画在 24 的 viewBox 上，渲染到 12–18px 时
   实际描边落在 0.80–1.20px，同一屏里重量差 50%。non-scaling-stroke
   把描边从 viewBox 缩放里摘出来，于是 stroke-width 的数值就是最终
   屏幕像素 —— 一条规则同时管住 chrome 里的图标和画布里随缩放的图标。
   选择器锚在 viewBox 上：连线工具栏的 24×14 迷你图和模拟网页里的
   图形都不是这个 viewBox，不会被波及。                              */
svg[viewBox="0 0 24 24"] * {
  stroke-width: var(--icon-stroke);
  vector-effect: non-scaling-stroke;
}

/* ---------- §4.7 滚动条 ----------
   15px 的系统滚动条压在每张白网页右缘上。拇指用 alpha 而不是固定灰，
   同一条规则才能同时成立于深色面板和白网页。                        */
* { scrollbar-width: thin; scrollbar-color: var(--alpha-4) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--alpha-3);
  border: 3px solid transparent;   /* 内缩出留白，拇指看起来是浮在槽里的 */
  background-clip: padding-box;
  border-radius: var(--r-full);
}
::-webkit-scrollbar-thumb:hover { background: var(--alpha-4); background-clip: padding-box; }
/* 网页内容是白底且不跟随主题，滚动条也不能跟 */
.card.web .page-scroll, .peek-body .page-scroll, .expand-body .page-scroll { scrollbar-color: var(--on-page-ink) transparent; }
.card.web .page-scroll::-webkit-scrollbar-thumb,
.peek-body .page-scroll::-webkit-scrollbar-thumb,
.expand-body .page-scroll::-webkit-scrollbar-thumb { background: var(--on-page-ink); background-clip: padding-box; }
/* 出现时不推动布局 */
.outline-list, .peek-body, .search-list, .modal-body { scrollbar-gutter: stable; }
/* Quiet until wanted (FigJam, Notion): with a pointer that hovers, a scroll area's
   thumb shows only while the pointer is over that area — which is whenever a wheel or
   a trackpad could be scrolling it. At rest the sidebar, the panels and every page sat
   striped with thumbs that said nothing. The gutter is kept, so nothing shifts. */
@media (hover: hover) {
  *:not(:hover) { scrollbar-color: transparent transparent; }
  *:not(:hover)::-webkit-scrollbar-thumb { background: transparent; }
  /* the pages' own thumbs are set more specifically above */
  :is(.card.web, .peek-body, .expand-body) .page-scroll:not(:hover) { scrollbar-color: transparent transparent; }
  :is(.card.web, .peek-body, .expand-body) .page-scroll:not(:hover)::-webkit-scrollbar-thumb { background: transparent; }
}

/* ---------- §4.7 微观基元 ---------- */
:root { accent-color: var(--accent); caret-color: var(--accent); }
::selection { background: color-mix(in oklch, var(--accent) 26%, transparent); }
/* 网页内容有自己的世界，选中色用页面墨色而不是应用强调色 */
.page-scroll ::selection { background: color-mix(in oklch, var(--accent) 22%, transparent); color: var(--text-page); }
body { font-optical-sizing: auto; }
.card.text .text-body, .pg p, .peek-foot, .empty p { text-wrap: pretty; }
.empty h2, .lod-title, .modal-head, .modal.confirm h3 { text-wrap: balance; }

/* Action surfaces need predictable contrast over both white pages and dark
   boards. Reserve translucency for orientation cues, rather than menu text. */
.popover, .card-toolbar, .link-toolbar {
  background: var(--surface-overlay);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.tip, .present-exit, .offscreen-beacon, .canvas-hint, .minimap {
  background: color-mix(in oklch, var(--surface-overlay) var(--overlay-opacity), transparent);
  backdrop-filter: blur(20px) saturate(1.7);
  -webkit-backdrop-filter: blur(20px) saturate(1.7);
}
.tip { background: color-mix(in oklch, var(--surface-inverted) 90%, transparent); }
/* Side Peek's header sits on the panel's own opaque surface with nothing passing under
   it, so it takes no blur: the backdrop filter only made a layer that was composited
   again on every frame the board under the panel moved. */
.expand-head { background: var(--reader-bg); backdrop-filter: none; -webkit-backdrop-filter: none; }
.modal-backdrop { backdrop-filter: blur(4px) saturate(1.1); -webkit-backdrop-filter: blur(4px) saturate(1.1); }
/* 系统关掉透明效果时退回实色 —— 无障碍要求，不是可选项 */
@media (prefers-reduced-transparency: reduce) {
  .popover, .card-toolbar, .link-toolbar, .present-exit, .offscreen-beacon, .canvas-hint, .minimap {
    background: var(--surface-overlay); backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  .tip { background: var(--surface-inverted); backdrop-filter: none; -webkit-backdrop-filter: none; }
  .peek-head { background: var(--surface-raised); backdrop-filter: none; -webkit-backdrop-filter: none; }
  .expand-head { background: var(--reader-bg); backdrop-filter: none; -webkit-backdrop-filter: none; }
  .modal-backdrop { backdrop-filter: none; -webkit-backdrop-filter: none; }
}

/* ---------- §4.4 卡片体与标题栏的接缝 ----------
   这里原本有一条 inset 0 1px 的 --on-page-ink（40% 墨）压在卡片体顶
   上，注释写的是「深色标题栏直接切到纯白页面，接缝是硬的；给白色一个
   落脚点」—— 那是给症状打的补丁：标题栏站在 app 平面、页面站在页面平
   面，两块颜色对不上，只好拿一条重线把断口盖住。
   现在标题栏已经归到页面平面，两边同色，断口本身不存在了，这条线就只
   剩「一道 40% 的黑杠」这一个作用。接缝交给 .card-head 自己那条 8% 、
   两端渐隐的 --head-seam 就够了。                                 */

/* ---------- §4.8 hover 快进慢出 ----------
   响应要快，消失要慢 —— 这是界面「跟手」最省力的一招。            */
.icon-btn, .text-btn, .chb, .ol-item, .popover button, .pop-item,
.card-toolbar button, .link-toolbar button, .site, .note-item, .peek-src, .offscreen-beacon {
  transition-duration: var(--dur-base);
}
.icon-btn:hover, .text-btn:not(:disabled):hover, .chb:hover, .ol-item:hover, .popover button:not(:disabled):hover, .pop-item:not(:disabled):hover,
.card-toolbar button:hover, .link-toolbar button:hover, .site:hover, .note-item:hover, .peek-src:hover, .offscreen-beacon:hover {
  transition-duration: var(--dur-fast);
}

/* ---------- §4.8 popover 从锚点长出来 ----------
   统一的「从下往上飘 6px」让每个菜单都像是从同一个地方来的；
   按放置方向设原点后，菜单是从按下的那个按钮里展开的。            */
@keyframes pop-open { from { opacity: 0; scale: var(--motion-scale); } }
.popover { animation: pop-open var(--dur-base) var(--ease-out); transform-origin: top left; }
.popover.closing { animation: scale-out var(--dur-fast) var(--ease-in) forwards; }
.popover[data-place="above"]       { transform-origin: bottom left; }
.popover[data-place="above-right"] { transform-origin: bottom right; }
.popover[data-place="below-right"] { transform-origin: top right; }

/* ---------- add-menu submenus ---------- */
.submenu { min-width: 220px; }
.add-menu.submenu-covered { visibility: hidden; pointer-events: none; }
.popover { max-width: calc(100vw - 16px); max-height: calc(100vh - 16px); max-height: calc(100dvh - 16px); overflow-y: auto; overscroll-behavior: contain; }
.add-menu button.sub-open { background: var(--state-hover); }
.add-menu button.sub-open .chev-r svg { color: var(--text-primary); }
.submenu .fav { width: var(--icon-md); height: var(--icon-md); border-radius: var(--r-xs); }
.sw-dot { width: 14px; height: 14px; border-radius: 50%; flex: none; box-shadow: inset 0 0 0 1px var(--border-default); }
.sw-dot.sw-dark { background: var(--note-dark-solid); } .sw-dot.sw-blue { background: var(--note-blue-solid); } .sw-dot.sw-teal { background: var(--note-teal-solid); } .sw-dot.sw-amber { background: var(--note-amber-solid); } .sw-dot.sw-rose { background: var(--note-rose-solid); }
.submenu .t-meta { font-size: var(--fs-xs); color: var(--text-tertiary); }

/* ---------- where Pin will put the page ---------- */
.pin-ghost {
  position: absolute; left: 0; top: 0; pointer-events: none;
  border: calc(1.5 * var(--px)) dashed var(--accent); border-radius: var(--card-r);
  background: color-mix(in oklch, var(--accent) 7%, transparent);
  display: grid; place-items: center; animation: fade-in var(--dur-fast) var(--ease-out);
}
.pin-ghost span { font-size: calc(12 * var(--px)); font-weight: var(--fw-semibold); color: var(--accent-text); padding: calc(4 * var(--px)) calc(10 * var(--px)); border-radius: var(--r-full); background: var(--surface-overlay); box-shadow: var(--shadow-sm); }

/* ---------- a drop in progress ---------- */
.viewport.drop-over::after {
  content: "Drop to add it to the board"; position: absolute; inset: var(--sp-3); z-index: var(--z-overlay);
  display: grid; place-items: start center; padding-top: var(--sp-5);
  border: 2px dashed var(--accent); border-radius: var(--r-xl); background: color-mix(in oklch, var(--accent) 6%, transparent);
  color: var(--accent-text); font-weight: var(--fw-semibold); pointer-events: none; animation: fade-in var(--dur-fast);
}

/* ---------- sub-canvas card ---------- */
.card.subcanvas { background: var(--surface-raised); }
.card.subcanvas .card-head { color: var(--text-secondary); }
.sub-body { background: var(--surface-sunken); background-image: radial-gradient(var(--grid-dot) 1px, transparent 1.2px); background-size: 16px 16px; display: flex; flex-direction: column; cursor: default; }
/* The preview is a live miniature of the nested board: real card markup, real
   pages, laid out at full size inside .sub-stage and scaled to fit by a single
   transform. Two things make that safe to put inside a card:
   `--px: 1px` pins the nested chrome to its design size so it scales WITH the
   content instead of staying screen-constant (a 32px title bar over a page
   drawn at a third of that is the bug you get otherwise), and pointer-events
   are off, so the whole thing behaves as one picture — the card's own
   double-click still opens the board. */
.sub-preview { position: relative; overflow: hidden; width: 100%; flex: 1; min-height: 0; }
.sub-stage { --px: 1px; --ui: 1; position: absolute; left: 0; top: 0; width: 0; height: 0; transform-origin: 0 0; pointer-events: none; user-select: none; }
.sub-stage .card { animation: none; transition: none; }
.sub-stage .card-head { cursor: default; }
.sub-stage .page-host, .sub-stage .page-scroll { overflow: hidden; }
.sub-stage .sub-links { position: absolute; left: 0; top: 0; width: 1px; height: 1px; overflow: visible; stroke: var(--link-stroke); stroke-width: 2; fill: none; }
.sub-meta { flex: none; height: 26px; display: flex; align-items: center; padding: 0 12px; font-size: var(--fs-xs); color: var(--text-tertiary); border-top: 1px solid var(--border-subtle); }
.card.subcanvas .lod-cover { background: var(--surface-raised); color: var(--text-primary); }
.cards.lod .card.subcanvas .sub-body { opacity: 0; content-visibility: hidden; }

/* ---------- the board trail (top bar, left) ----------
   The board name and every breadcrumb are the SAME chip: one height, one inner
   padding, one radius. The trail's gap is the top bar's own gap, so there is
   nothing to compensate for — and that is what makes each chevron sit optically
   centred, because text→chevron and chevron→text are both `padding + gap`.
   The old rule had three values for one rhythm (name `0 8px 0 10px`, crumbs
   `0 6px`, and a `margin-left: -2px` on the trail to undo the parent's gap),
   which put every chevron 2px closer to the label on its right. */
.crumbs { display: flex; align-items: center; gap: var(--sp-1); min-width: 0; }
.crumb-sep { display: flex; flex: none; }
.crumb-sep svg { width: var(--icon-sm); height: var(--icon-sm); color: var(--text-tertiary); }
.crumb { display: inline-flex; align-items: center; height: var(--ctl-md); padding: 0 var(--sp-2); border-radius: var(--r-sm); color: var(--text-secondary); font-weight: var(--fw-medium); max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
button.crumb:hover { background: var(--state-hover); color: var(--text-primary); }
.crumb.current { color: var(--text-primary); font-weight: var(--fw-semibold); }
/* Inside a sub-canvas the trail has a current item, so the ancestors above it —
   the board name included — step down to medium. Two semibold labels in one
   trail is two answers to "where am I". */
.app.in-sub .board-name { color: var(--text-secondary); font-weight: var(--fw-medium); }
.app.in-sub .board-name:hover { color: var(--text-primary); }
/* Nested, the name is not a menu — clicking it goes back to the root board — so
   the disclosure chevron has to go, or it promises a dropdown and navigates. */
.app.in-sub .board-name .chev { display: none; }
/* Same grid as a row, so the chevron lands in the icon column and the label
   starts on the same x as every title under it. */
.ol-back { display: grid; grid-template-columns: 18px 1fr; align-items: center; gap: var(--sp-2); width: 100%; height: var(--row-sm); padding: 0 var(--sp-2); margin-bottom: var(--sp-2); border-radius: var(--r-sm); color: var(--text-secondary); font-weight: var(--fw-medium); text-align: left; transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); }
.ol-back:hover { background: var(--state-hover); color: var(--text-primary); }
.ol-back svg { width: var(--icon-sm); height: var(--icon-sm); justify-self: center; }
.ol-back span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* templates gallery */
.modal.wide { width: 820px; }
/* A template is the same tile as one on Home (a board, a template, a page in the
   Library): the picture is the object — its corner, its hairline, a lift under the
   pointer — with the name and a line under it. It used to be a filled card with the
   picture inset, a second way of drawing the same thing. */
.tpl-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-6) var(--sp-4); }
.tpl { display: flex; flex-direction: column; min-width: 0; padding: 0; border-radius: var(--r-lg); background: none; text-align: left; }
.tpl:focus-visible { outline: none; }
.tpl:focus-visible .tpl-preview { outline: 2px solid var(--accent-line); outline-offset: 2px; }
.tpl:hover .tpl-preview { border-color: var(--border-default); box-shadow: var(--shadow-md); }
.tpl:active .tpl-preview { box-shadow: var(--shadow-sm); }
.tpl-preview { border-radius: var(--r-lg); border: 1px solid var(--border-subtle); transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out); background: var(--surface-sunken); background-image: radial-gradient(var(--grid-dot) 1px, transparent 1.2px); background-size: 12px 12px; display: grid; place-items: center; overflow: hidden; }
.tpl-preview svg { display: block; width: 100%; height: auto; }
.tpl-preview .tp-frame { fill: none; stroke: var(--border-strong); }
.tpl-preview .tp-link { stroke: var(--link-stroke); stroke-width: 1; }
.tpl-preview .tp-web { fill: var(--surface-page); stroke: var(--border-default); stroke-width: 0.5; }
.tpl-preview .tp-head { fill: var(--fill-strong); }
.tpl-preview .tp-dark { fill: var(--note-dark-solid); } .tpl-preview .tp-blue { fill: var(--note-blue-solid); } .tpl-preview .tp-teal { fill: var(--note-teal-solid); } .tpl-preview .tp-amber { fill: var(--note-amber-solid); } .tpl-preview .tp-rose { fill: var(--note-rose-solid); }
.tpl-name { margin-top: 10px; padding: 0 2px; font-weight: var(--fw-medium); }
.tpl-desc { margin-top: 2px; padding: 0 2px; font-size: var(--fs-sm); letter-spacing: var(--ls-12); color: var(--text-secondary); line-height: var(--lh-snug); }
.modal-head .beta { margin-left: 6px; vertical-align: 2px; }

/* ---------- phone width ----------
   Below 720px there is no room for a 260px rail beside the board: it left the
   canvas 115px wide and pushed Share, Search and More off the screen, so the whole
   page scrolled sideways. The outline becomes a drawer over the board (closed by
   default, closed again by a pick or a tap on the board), and the top bar keeps
   only what it needs: the outline toggle, the board name, Share, Search, More. */
@media (max-width: 720px) {
  .app { position: relative; overflow: hidden; }
  .outline { width: min(280px, 86vw); }
  #btn-outline-close { display: grid; }
  .topbar { grid-template-columns: minmax(0, 1fr) auto; padding: 0 var(--sp-2); }
  .topbar-center, .topbar .traffic, .topbar .traffic + .divider, .topbar-right .divider { display: none; }
  .topbar-left { min-width: 0; }
  .board-name { min-width: 0; }
  .board-name #board-name-text { max-width: none; min-width: 0; }
  .bottombar #btn-minimap, .bottombar .vsep { display: none; }
  .site-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .empty { padding: 24px; overflow-y: auto; }
  .empty-art { flex-shrink: 0; }
  .tpl-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Below 360px the zoom readout no longer fits beside the tools: pinch zooms, and ⌘K
   (Search) still has Zoom to fit, 100% and selection. */
@media (max-width: 359px) { .bottombar .zoom-pct { display: none; } }
/* Read-only boards have fewer tools and still need a touch-accessible way to
   find content after resizing or returning from a nested canvas. */
@media (max-width: 359px) { .board-viewonly .bottombar .zoom-pct { display: inline-flex; align-items: center; justify-content: center; } }

@media (max-width: 480px) {
  .modal-foot:not(.right) { flex-direction: column; align-items: stretch; }
  .modal-foot .hint { line-height: var(--lh-base); }
  .modal-foot:not(.right) .text-btn { width: 100%; }
  .shortcuts { grid-template-columns: minmax(0, 1fr); }
  .tpl-grid { grid-template-columns: minmax(0, 1fr); }
  .pl-item { grid-template-columns: 18px minmax(0, 1fr) auto; gap: 3px 10px; }
  .pl-item > .fav, .pl-item > .ti { grid-row: auto; }
  .pl-sub { max-width: 100%; }
  .palette-foot span { white-space: nowrap; }
  .recent { grid-template-columns: 18px minmax(0, 1fr); }
  .recent-host { grid-column: 2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .empty-actions { width: 100%; }
  .empty-actions .text-btn { width: 100%; }
  /* The remembered desktop width must not leave a sliver of canvas on a phone. */
  .app.web .peek { top: 0; bottom: 0; width: 100% !important; max-width: 100%; border-radius: 0; border-left: 0; }
  .app.web .peek-resize { display: none; }
  .peek-foot { flex-wrap: wrap; }
  .peek-from { flex: 1; }
}

@media (pointer: coarse) {
  .app .topbar { height: calc(52px + env(safe-area-inset-top)); padding-top: env(safe-area-inset-top); }
  .app .bottombar { min-height: calc(52px + env(safe-area-inset-bottom)); height: calc(52px + env(safe-area-inset-bottom)); padding-bottom: env(safe-area-inset-bottom); }
  .app .topbar .icon-btn, .app .topbar .board-name, .outline-head .icon-btn, .app .bottombar .icon-btn, .app .bottombar .zoom-pct, .app .bottombar .add-btn { min-width: 44px; min-height: 44px; }
  .popover button, .popover a.pop-item, .modal-head .icon-btn, .modal-foot .text-btn, .palette-input .icon-btn, .side-item, .ol-item, .ol-section { min-height: 44px; }
  .modal-head .icon-btn, .palette-input .icon-btn { min-width: 44px; }
  .empty-actions .text-btn, .peek-foot .text-btn { min-height: 44px; }
  .peek-nav .icon-btn, .peek-actions .icon-btn { min-width: 44px; min-height: 44px; }
  .field input, .share-row input, .rename-input, .palette-input input, .palette-tools select { font-size: var(--fs-xl); }
  .field input, .share-row input, .rename-input, .palette-tools select { min-height: var(--ctl-xl); }
  .seg-text button, .popover .seg button { min-height: var(--ctl-xl); }
  .link-toolbar > button { min-width: var(--ctl-xl); min-height: var(--ctl-xl); }
  .palette-foot { display: none; }
}

@media (max-height: 480px) {
  .empty { justify-content: flex-start; padding: 16px 24px; }
  .empty-art, .empty-hint { display: none; }
}

/* Hosted app: who is signed in, at the top of the ··· menu. */
.pop-account { display: flex; align-items: center; gap: var(--sp-2); padding: 6px var(--sp-2) var(--sp-2); min-width: 220px; }
.pop-avatar { width: var(--row-sm); height: var(--row-sm); border-radius: 50%; flex: none; object-fit: cover; display: grid; place-items: center; background: var(--accent); color: var(--text-on-accent); font-size: var(--fs-xs); font-weight: var(--fw-semibold); }
.pop-who { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
.pop-who b { font-weight: var(--fw-semibold); color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pop-who span { font-size: var(--fs-xs); color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A destructive item is marked the one way every menu marks it (.popover button.danger):
   red under the pointer, plain at rest, so a menu doesn't shout about its last row. */
/* /app could not fetch the board and this device has no copy to start from. */
.boot-error { position: fixed; inset: 0; z-index: 1000; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-3); padding: 16px; text-align: center; background: var(--surface-base); color: var(--text-primary); }
.boot-error h1 { margin: 0; font-size: var(--fs-xl); }
.boot-error p { margin: 0; color: var(--text-secondary); max-width: 36em; }
.boot-error div { display: flex; gap: var(--sp-2); margin-top: var(--sp-2); }
.boot-error a { display: inline-flex; align-items: center; text-decoration: none; }

/* ---------- boot (the hosted app, while it signs in and fetches the board) ----------
   app.html is built with .booting and a .boot-loader. Until app.js has drawn the board
   the chrome holds nothing true (a placeholder title, unlabelled buttons), so it is
   held back and the mark breathes in the middle of the canvas — only once loading has
   taken long enough to notice (250ms), so a quick boot shows nothing at all. */
.app.booting :is(.outline, .topbar, .bottombar) > * { opacity: 0; }
.app.revealing :is(.outline, .topbar, .bottombar) > * { animation: fade-in var(--dur-slow) var(--ease-out) both; }
/* leaving Present: the chrome comes back as a fade, not a cut */
.app.chrome-in :is(.outline, .topbar, .bottombar) { animation: fade-in var(--dur-slow) var(--ease-out) both; }
.boot-loader { position: fixed; inset: 44px 0 44px 260px; z-index: var(--z-modal); display: grid; place-items: center; pointer-events: none; color: var(--accent); animation: fade-in 420ms var(--ease-out) 250ms both; }
@media (max-width: 720px) { .boot-loader { left: 0; } } /* the middle of the canvas, beside the sidebar */
.app:not(.booting) .boot-loader { opacity: 0; scale: 0.94; transition: opacity var(--dur-base) var(--ease-in), scale var(--dur-base) var(--ease-in); animation: none; }
.boot-loader svg { width: 40px; height: 40px; overflow: visible; }
.boot-loader path { animation: 1.6s var(--ease-in-out) infinite; }
.boot-loader .bl-a { animation-name: bl-a; }
.boot-loader .bl-b { animation-name: bl-b; }
/* the two halves of the mark part and meet again — the board assembling */
@keyframes bl-a { 50% { transform: translate(5px, -5px); opacity: 0.45; } }
@keyframes bl-b { 50% { transform: translate(-5px, 5px); opacity: 0.45; } }

/* ---------- web chrome (.app.web — the published app) ----------
   The local prototype imitates a native window: traffic lights in the bar. The web
   app is a page in a browser tab, and follows Notion's web app (measured on
   notion.site): the bar is 44px with no fill or rule of its own, the title sits in
   regular weight and icon buttons are 28px. The logo opens application settings;
   Outline contains only the board's content navigation. */
.app.web .traffic, .app.web .traffic + .divider { display: none; }
.app.web .topbar { background: var(--surface-base); border-bottom-color: var(--border-subtle); padding: 0 var(--sp-2) 0 var(--sp-3); }
.app.web .topbar-center { display: none; }
.app.web .topbar { grid-template-columns: minmax(0, 1fr) auto; }
.app.web .board-name { font-weight: var(--fw-regular); height: var(--row-sm); padding: 0 6px; }
.app.web .topbar-right { gap: 2px; }
.app.web .topbar-right .icon-btn { width: var(--row-sm); height: var(--row-sm); }


/* One icon-only application menu, always available beside the board title. */
.account-switch { display: grid; place-items: center; flex: none; width: 34px; height: 34px; margin: 0; padding: 0; border-radius: var(--r-control); color: var(--text-primary); transition: background var(--dur-fast) var(--ease-out); }
.account-switch:hover, .account-switch[aria-expanded="true"] { background: var(--state-hover); }
.account-switch:focus-visible, .side-item:focus-visible { outline: 2px solid var(--accent-line); outline-offset: -2px; }
.avatar-face { width: 22px; height: 22px; border-radius: 6px; flex: none; display: grid; place-items: center; object-fit: cover; background: var(--accent); color: var(--text-on-accent); font-size: var(--fs-2xs); font-weight: var(--fw-semibold); letter-spacing: 0; }
.account-pop { box-sizing: border-box; width: 272px; min-width: min(248px, calc(100vw - 16px)); }
.account-pop .pop-account { min-width: 0; }
.account-pop .pop-who { flex: 1; }
.account-pop .pop-field { display: block; padding: 6px 8px 8px; }
.account-pop .pop-field-label { display: block; margin-bottom: 6px; }
.account-pop .seg { display: flex; width: 100%; }
.account-pop .seg button { flex: 1; justify-content: center; min-width: 0; }
.account-help summary { display:flex;align-items:center;justify-content:space-between;min-height:32px;padding:0 8px;border-radius:var(--r-control);cursor:pointer;list-style:none;color:var(--text-primary); }
.account-help summary::-webkit-details-marker { display:none; }
.account-help summary:hover { background:var(--state-hover); }
.account-help summary:focus-visible { outline:2px solid var(--accent-line);outline-offset:-2px; }
.account-help summary .chev { display:flex; }
.account-help summary svg { width:12px;height:12px;color:var(--text-secondary); }
.account-help[open] summary svg { transform:rotate(180deg); }
@media (max-width: 720px), (pointer: coarse) {
  .account-switch { min-height: 44px; }
  .account-help summary { min-height:44px; }
}
a.pop-item { text-decoration: none; color: var(--text-primary); }

/* Sidebar rows above the outline, as Notion's Search / Home / Inbox. */
.side-nav { padding: 6px var(--sp-2) 2px; min-width: 260px; }
.side-item { display: flex; align-items: center; gap: var(--sp-2); width: 100%; height: var(--row-sm); padding: 0 var(--sp-2); border-radius: var(--r-sm); color: var(--text-secondary); font-weight: var(--fw-medium); transition: background var(--dur-fast) var(--ease-out); }
.side-item:hover { background: var(--state-hover); color: var(--text-primary); }
.side-item .pi { display: grid; place-items: center; }
.side-item .pi svg { width: var(--icon-md); height: var(--icon-md); }
.side-item span:not(.pi) { flex: 1; text-align: left; }
.side-item kbd { font-family: var(--font-ui); font-size: var(--fs-2xs); color: var(--text-tertiary); opacity: 0; transition: opacity var(--dur-fast); }
.side-item:hover kbd, .side-item:focus-visible kbd { opacity: 1; }
@media (max-width: 720px) {
  .app.web .save-state:not(.failed) { display: none; } /* a failure still shows */
}

/* ---------- Side Peek with nothing open yet: a new tab ----------
   The panel's own page, not somebody else's: a search field and a line on how to
   put a page here. The published app opens the panel here rather than on a
   third-party home page, because the ones worth opening (Bing's, Google's) refuse
   to be framed and would have shown as a preview. Results come from Bing, which
   allows framing of its search pages. */
/* A single input anchors the empty tab; supporting copy stays secondary. */
.peek-newtab { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 32px 24px 72px; background: var(--surface-raised); color: var(--text-primary); overflow-y: auto; }
.peek-newtab-content { width: min(100%, 400px); }
.peek-newtab label { display: block; font-size: var(--fs-2xl); letter-spacing: var(--ls-20); font-weight: var(--fw-semibold); line-height: var(--lh-tight); }
.peek-newtab p { margin: 8px 0 0; color: var(--text-secondary); font-size: var(--fs-sm); line-height: var(--lh-base); }
.peek-newtab form { margin-top: 24px; display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 5px 6px 5px 14px; border-radius: var(--r-md); background: var(--fill-subtle); border: 1px solid var(--border-default); transition: border-color var(--dur-fast), box-shadow var(--dur-fast); }
.peek-newtab form:has(input:focus) { border-color: var(--field-focus-border); box-shadow: var(--ring-focus); background: var(--surface-raised); }
.peek-newtab .pi { color: var(--text-tertiary); }
.peek-newtab .pi svg { width: 16px; height: 16px; }
.peek-newtab input { flex: 1; min-width: 0; background: none; border: 0; outline: 0; color: var(--text-primary); font-size: 16px; }
.peek-newtab input::placeholder { color: var(--text-tertiary); }
.peek-newtab form button { display: grid; place-items: center; flex: none; width: 36px; height: 36px; border-radius: var(--r-sm); background: var(--action-solid-bg); color: var(--action-solid-fg); transition: opacity var(--dur-fast), background var(--dur-fast); }
/* Nothing typed yet: a quiet square on the field, not the solid accent at a third of
   its strength (a muddy blue block in dark). */
.peek-newtab form button:disabled { background: var(--fill-strong); color: var(--text-tertiary); opacity: 1; }
.peek-newtab form button:not(:disabled):hover { background: var(--action-solid-bg-hover); }
.peek-newtab .peek-newtab-hint { margin-top: 12px; color: var(--text-tertiary); font-size: var(--fs-xs); }

/* Side Peek tabs keep their live documents mounted when inactive. The strip reads as
   macOS draws one: the tab in front is a raised thumb on the chrome (the selected
   segment's --control-thumb), the others are labels on it. It scrolls sideways with no
   scrollbar — a thin one appearing pushed the whole panel down by its height — and an
   edge with more tabs past it fades (app.js fadePeekStrip). Titles fade out instead
   of ending in "…", so a narrow tab keeps a few more letters. */
.peek-tabs-row { display: flex; flex: none; align-items: center; gap: 4px; height: 40px; padding: 0 6px 0 8px; background: var(--surface-chrome); border-bottom: 1px solid var(--border-subtle); }
.peek-tabs { position: relative; display: flex; flex: 1; min-width: 0; align-self: stretch; align-items: center; gap: 2px; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; }
.peek-tabs::-webkit-scrollbar { display: none; }
.peek-tabs.fade-start { mask-image: linear-gradient(to right, transparent, #000 28px); }
.peek-tabs.fade-end { mask-image: linear-gradient(to left, transparent, #000 28px); }
.peek-tabs.fade-start.fade-end { mask-image: linear-gradient(to right, transparent, #000 28px, #000 calc(100% - 28px), transparent); }
.peek-tab { display: flex; flex: 0 1 184px; min-width: 104px; height: 28px; align-items: center; gap: 2px; padding: 0 4px 0 0; border: 0; border-radius: var(--r-sm); color: var(--text-secondary); }
.peek-tab:hover { background: var(--state-hover); }
.peek-tab.active, .peek-tab.active:hover { background: var(--control-thumb); box-shadow: var(--control-thumb-shadow); color: var(--text-primary); }
.peek-tab [role="tab"] { display: flex; align-items: center; flex: 1; gap: 7px; min-width: 0; height: 100%; padding: 0 2px 0 8px; border-radius: inherit; text-align: left; font-size: var(--fs-sm); letter-spacing: var(--ls-12); }
.peek-tab-title { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; mask-image: linear-gradient(to left, transparent, #000 14px); }
.peek-tab.preview .peek-tab-title { font-style: italic; }
@media (min-width: 701px) { html.desktop .app.outline-open #web-reader { padding-left: var(--outline-w, 260px); } }
.peek-tab-saved { display: grid; flex: none; place-items: center; color: var(--text-tertiary); }
.peek-tab-saved svg { width: 12px; height: 12px; }
.peek-tab-menu { position: absolute; z-index: 1200; width: 204px; padding: 4px; }
.peek-tab-menu button { display: block; width: 100%; padding: 9px 12px; border-radius: var(--r-sm); text-align: left; color: var(--text-primary); font-size: var(--fs-sm); }
.peek-tab-menu button:hover, .peek-tab-menu button:focus-visible { background: var(--state-hover); }
.peek-tab-menu button:disabled { color: var(--text-tertiary); }
.peek-recent { margin-top: 28px; }
.peek-recent > p { display: flex; justify-content: space-between; }
.peek-recent > p span { font-size: var(--fs-xs); color: var(--text-tertiary); }
.peek-newtab .peek-recent button { display: block; width: 100%; height: auto; min-height: 36px; margin-top: 4px; padding: 8px 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; background: transparent; color: var(--text-secondary); font-size: var(--fs-sm); }
.peek-newtab .peek-recent button:hover { background: var(--state-hover); }
.peek-tab-icon { display: grid; place-items: center; flex: none; color: var(--text-tertiary); }
.peek-tab-icon svg, .peek-tab-icon img { width: 14px; height: 14px; }
.peek-tab-close { display: grid; place-items: center; flex: none; width: 20px; height: 20px; border-radius: var(--r-xs); color: var(--text-secondary); opacity: 0; }
.peek-tab.active .peek-tab-close, .peek-tab:hover .peek-tab-close, .peek-tab:focus-within .peek-tab-close { opacity: 1; }
.peek-tab-close svg { width: var(--icon-xs); height: var(--icon-xs); }
.peek-tab-close:hover { background: var(--fill-strong); color: var(--text-primary); }
#peek-new-tab { flex: none; width: 30px; height: 30px; }
/* With several tabs the panel's own controls end the strip (app.js renderPeekTabs): +
   beside the tabs it makes, a hairline, then Close Side Peek — a panel glyph, since it
   only hides the panel and its tabs stay, kept apart from each tab's ×. */
.peek-tabs-sep { flex: none; width: 1px; height: 16px; margin: 0 3px; background: var(--border-default); }
.peek-tabs-row > :is(#peek-new-tab, #peek-close) { flex: none; width: 28px; height: 28px; }
#peek-new-tab svg { width: 16px; height: 16px; }

.peek-tab-panel { position: absolute; inset: 0; overflow: hidden; }
.app.web .peek-body { scrollbar-gutter: auto; }
.app.web .peek-head { height: 48px; padding: 0 8px; gap: 6px; background: var(--surface-raised); }
.app.web #peek-url { height: 32px; padding: 0 8px; }
/* The reader can be narrow on any screen after its grip is resized. */
@container (max-width: 480px) {
  .app.web .peek-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; height: auto; padding: 8px; gap: 6px; }
  .app.web #peek-url { grid-column: 1 / -1; grid-row: 1; width: 100%; }
  .app.web .peek-head > .peek-nav { grid-column: 1; grid-row: 2; }
  .app.web .peek-head > .peek-actions { grid-column: 2; grid-row: 2; }
  .peek-foot { flex-wrap: wrap; }
  .peek-from { flex: 1; }
}
#peek-url-text { flex: 1; cursor: text; }
/* Quiet site identity at rest; the complete address appears when editing. */
#peek-fav, #expand-fav { display: none; }
#peek-url-text, #expand-url-text { flex: 1; min-width: 0; text-align: center; cursor: text; color: var(--text-secondary); }
#expand-url-text { color: var(--reader-ink); }
#peek-url, #expand-url { --address-action-size: 28px; padding-inline: 3px; gap: 4px; cursor: text; }
/* Balance the trailing action so the site stays centered in the whole field. */
#peek-url::before, #expand-url::before { content: ""; flex: 0 0 var(--address-action-size); }
.peek-url .address-reload { flex: none; width: var(--address-action-size); height: var(--address-action-size); border-radius: var(--r-sm); cursor: pointer; }
.peek-url .address-reload svg { width: var(--icon-sm); height: var(--icon-sm); }
#peek-url:has(form:not([hidden]))::before, #expand-url:has(form:not([hidden]))::before { display: none; }
#peek-url form, #expand-url form { padding-inline-start: 8px; }
#peek-url:has(form:not([hidden])), #expand-url:has(form:not([hidden])) { cursor: default; }
#peek-url:has(input:focus) { border-color: var(--field-focus-border); }
/* Keyboard focus on the address is drawn by the field, not by the text inside it: the
   text's own outline was a square box inside the rounded field. */
#peek-url-text:focus-visible { outline: none; }
#peek-url:has(#peek-url-text:focus-visible), #expand-url:has(#expand-url-text:focus-visible) { border-color: var(--field-focus-border); box-shadow: var(--ring-focus); }
/* A new tab's search field is its address bar (app.js editPeekAddress sends ⌘L there):
   the header's address and reload stand down rather than offer a second place to type.
   Hidden, not removed, so nothing in the header moves when the first page opens. */
.peek.on-newtab :is(#peek-url, #peek-reader) { visibility: hidden; }
#peek-address-form { flex: 1; min-width: 0; }
#peek-address-form input { width: 100%; min-width: 0; padding: 0; border: 0; outline: 0; background: transparent; color: var(--text-primary); font-size: var(--fs-sm); }
@media (pointer: coarse) {
  #peek-url, #expand-url { --address-action-size: 44px; height: 46px; }
  .peek-tab [role="tab"], #peek-new-tab { min-width: 44px; min-height: 44px; }
  .peek-tab-close { width: 40px; height: 44px; opacity: 1; }
  .app.web .peek-head .icon-btn { width: 40px; height: 44px; }
  #peek-address-form input { font-size: 16px; }
  .peek-newtab form button { width: 44px; height: 44px; }
}
@media (max-width: 640px) { #expand-url { --address-action-size: 44px; height: 46px; } }
@media (max-width: 420px) { .peek-newtab { padding: 24px 20px 48px; } .peek-tab { min-width: 104px; } }

/* While brushing, indicate the hit objects without exposing editing controls.
   The transient rectangle and the committed selection have different jobs. */
/* .marqueeing is app.js setMarquee(): a class, not .viewport:has(> .marquee…), which
   made any change under the viewport re-check the board and restyle every card head. */
.viewport.marqueeing .card-head-actions,
.viewport.marqueeing .resize::after { opacity: 0; pointer-events: none; }
.viewport.marqueeing .card.selected .card-head { --head-fill: var(--card-chrome); }
.viewport.marqueeing .card.web.selected:not(.no-live) .card-head { --head-fill: var(--card-chrome-page); }
.viewport.marqueeing .card.selected .card-title { mask-image: none; text-overflow: ellipsis; }


/* Notes are writing surfaces; page cards retain their browser chrome. */
.card.text {
  --head-act-w: calc(26 * var(--px));
  --card-shadow: 0 0 0 transparent;
  --card-shadow-hover: var(--card-shadow);
  --card-top: inset 0 0 0 transparent;
}
.card.text .card-head {
  height: 44px; padding: 12px 18px 4px; gap: 8px;
  font-size: var(--fs-note-title); line-height: var(--lh-note-title); letter-spacing: var(--ls-note-title);
  color: var(--text-primary); background: none;
}
.card.text.untitled .card-head { height: 16px; padding: 0 18px; }
.card.text.untitled .card-head .card-title { visibility: hidden; }
.card.text .text-body {
  padding: 4px 18px 18px; font-size: var(--fs-note); line-height: var(--lh-note);
  letter-spacing: 0; overflow-wrap: anywhere;
}
.card.text .text-body p { margin: 0 0 0.65em; }
.card.text .text-body p:last-child { margin-bottom: 0; }
.card.text .text-body ul, .card.text .text-body ol { padding-left: 1.3em; margin: 0.5em 0; }
.card.text .text-body li + li { margin-top: 0.25em; }
/* Pasted documents retain structure in both Canvas and Doc. */
:is(.text-body,.web-reading-text) :is(h1,h2,h3,h4,h5,h6) { font-size:1.2em;line-height:1.4;margin:.8em 0 .45em; }
:is(.text-body,.web-reading-text) blockquote { margin:.65em 0;padding-left:1em;border-left:3px solid currentColor; }
:is(.text-body,.web-reading-text) pre { max-width:100%;margin:.7em 0;padding:14px;border-radius:8px;background:color-mix(in srgb,currentColor 6%,transparent);white-space:pre;overflow-x:auto;text-wrap:nowrap;tab-size:4; }
:is(.text-body,.web-reading-text) code { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em; }
:is(.text-body,.web-reading-text) pre code { white-space:inherit;font-size:.85em;line-height:1.6; }
:is(.text-body,.web-reading-text) img { display:block;max-width:100%;height:auto;margin:.75em 0;border-radius:6px; }
:is(.text-body,.web-reading-text) table { width:100%;margin:.8em 0;border-collapse:collapse;table-layout:fixed; }
:is(.text-body,.web-reading-text) :is(td,th) { padding:8px;border:1px solid color-mix(in srgb,currentColor 22%,transparent);vertical-align:top;overflow-wrap:anywhere; }
:is(.text-body,.web-reading-text) th { font-weight:600;text-align:left; }
:is(.text-body,.web-reading-text) hr { border:0;border-top:1px solid color-mix(in srgb,currentColor 22%,transparent);margin:1em 0; }
.card.text.editing { outline-color: var(--accent); }
.card-toolbar .note-color-dot { width: 16px; height: 16px; border-radius: 50%; border: 1px solid var(--border-strong); }
#ctx-menu [role="menuitemradio"][aria-checked="true"] { background: var(--state-selected); }
#ctx-menu [role="menuitemradio"][aria-checked="true"]::after { content: '✓'; margin-left: auto; }

/* Listed, not [class*="color-"]: an attribute selector on class makes every class change
   anywhere restyle every card head under it (~650 heads a click on a 304-card board). */
.card.text:is(.color-dark, .color-blue, .color-teal, .color-amber, .color-rose) .card-head { color: var(--text-primary); }
.card-toolbar button.toolbar-label { width: auto; padding: 0 10px; font-size: var(--fs-sm); letter-spacing: var(--ls-12); }
.card.text.untitled .card-head:has(.rename-input) { height: 44px; padding: 12px 18px 4px; }
/* No handles round a note or shape being typed in. Asked of the cards' own row only
   (`> .card`): as `.viewport:has(.card…)` every insertion anywhere on the board — a
   label, a handle, a keystroke's text node — re-ran the check over the whole board. */
.cards:has(> .card:is(.text, .shape).editing) ~ .anchors { visibility: hidden; }


.card.text.note-measure { position: fixed; left: -100000px; top: 0; height: auto; contain: none; container-type: normal; transform: none; visibility: hidden; pointer-events: none; --px: 1px; }
.note-measure .text-body { flex: none; height: auto; overflow: visible; }
.card.web.interacting .live-badge { font-size: calc(9 * var(--px)); letter-spacing: 0; text-transform: none; }
/* A live bar holds the title, the label, Done and three actions, all screen-constant, so
   on a card under ~460 screen px the title was squeezed to two letters ("Ho"). The title
   now keeps a few words, and the label gives up its word first and keeps its dot — Done,
   the ring and the tint already say the same thing. The word wraps onto a second line
   the label's height hides, so it goes whole, never cut mid-word. */
/* Live, the actions sit in the row rather than over the title, so the title has no fade
   to make room for them — the selected card's fade (five classes) outranked the plain
   live rule above and left a live bar with no title at all. */
.card.web.interacting > .card-head > .card-title { min-width: calc(64 * var(--px)); mask-image: none; text-overflow: ellipsis; }
.card.web.interacting .live-badge { flex: 0 1 auto; min-width: calc(6 * var(--px)); flex-wrap: wrap; align-content: flex-start; height: calc(12 * var(--px)); line-height: calc(12 * var(--px)); overflow: hidden; }
.card.web.interacting .live-badge::before { margin-block: calc(3 * var(--px)); }

/* The active page is explicit; surrounding research remains readable. */
.card.web.interacting { box-shadow: var(--card-elev-hover); }
.app.interacting .card:not(.interacting), .app.interacting .links .link:not(.related), .app.interacting .cards:not(.lod) ~ .link-labels { opacity: 0.72; }

/* Controls keep screen-sized hit areas while overview mode keeps the board quiet. */
.card-toolbar { width: max-content; flex-wrap: wrap; }
.card-toolbar > button, .card-toolbar > .tb-sep, .card-toolbar > .tb-count { flex-shrink: 0; }
.rz-handle::before { content: ""; position: absolute; width: 24px; height: 24px; left: 50%; top: 50%; transform: translate(-50%, -50%); }
.card.text.selected .card-title { mask-image: none; text-overflow: ellipsis; }
@media (pointer: coarse) {
  .card-toolbar > button { min-width: 44px; min-height: 44px; }
}
@media (max-width: 420px) {
  .card-toolbar .tb-count { flex-basis: 100%; padding: 2px 6px 4px; }
}

/* Keep connection tools inside narrow canvas viewports. */
.link-toolbar { width: max-content; flex-wrap: wrap; }
.link-toolbar > * { flex-shrink: 0; }
@media (pointer: coarse) { .link-toolbar button { min-width: 44px; min-height: 44px; } }
.link-toolbar button.sw { background: transparent; }
.link-toolbar button.sw::before { content: ""; width: 16px; height: 16px; border-radius: 50%; background: var(--swatch-color); }
.link-toolbar button.sw:hover { background: var(--state-hover); }
.link-toolbar button.sw.on::before { outline: 1.5px solid var(--text-primary); outline-offset: 3px; }

/* Selection tools: a clear primary action, compact secondary controls. */
/* A card's toolbar and a connection's are one component: the same frame, corner and
   targets (the connection's used to be the older 28px / 9px one). r-xl − 5px → r-md. */
.card-toolbar, .link-toolbar { gap: 3px; padding: 5px; border-radius: var(--r-xl); box-shadow: var(--shadow-md); }
.card-toolbar > button, .link-toolbar > button { width: var(--ctl-float); height: var(--ctl-float); border-radius: var(--r-md); }
.link-toolbar .tb-sep { height: 16px; margin: 0 4px; }
.card-toolbar button svg { width: 18px; height: 18px; }
.card-toolbar button.toolbar-label { display: flex; align-items: center; justify-content: center; gap: 7px; }
.card-toolbar .toolbar-edit { color: var(--text-primary); font-weight: var(--fw-medium); }
.card-toolbar .tb-sep { height: 16px; margin: 0 4px; }
.card-toolbar button[aria-expanded="true"] { background: var(--state-selected); color: var(--text-primary); }
.card-toolbar button:focus-visible, .link-toolbar button:focus-visible { outline: 2px solid var(--accent-line); outline-offset: 1px; }
.card-toolbar .note-color-dot { width: 15px; height: 15px; box-shadow: inset 0 0 0 1px rgb(255 255 255 / 12%); }
#ctx-menu.note-color-popover { width: 278px; min-width: 0; max-width: calc(100vw - 16px); padding: 10px; border-radius: var(--r-lg); }
.note-color-popover .pop-title { padding: 2px 4px 10px; }
.note-color-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 3px; }
.note-color-grid button { min-width: 0; height: 64px; padding: 6px 2px; display: flex; flex-direction: column; justify-content: center; gap: 7px; border-radius: var(--r-md); font-size: 10px; }
.note-color-grid button[aria-checked="true"] { background: var(--state-selected); color: var(--text-primary); }
.color-sample { display: grid; place-items: center; width: 25px; height: 25px; border-radius: 50%; flex-shrink: 0; }
.note-color-grid .color-sample svg { width: 16px; height: 16px; margin: 0; fill: none; stroke: var(--text-on-content-solid); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; visibility: hidden; }
.note-color-grid button[aria-checked="true"] svg { visibility: visible; }
@media (pointer: coarse) { .card-toolbar > button { min-width: 44px; min-height: 44px; } }

#ctx-menu.note-color-popover [role="menuitemradio"]::after { content: none; }

/* Frame names orient the canvas; note titles belong to the document.
   They are also how a board is read from a distance, so they stay 12 screen px at
   every zoom. --px stops compensating below 42% (card furniture must not outgrow its
   card), which left a region's name at 5px exactly when you zoom out to see regions.
   --fui is the uncapped reciprocal, written by cull() on frame nodes only — on .world
   it would restyle the whole board on every zoom step. Inside 42–400% it is unset and
   the name follows --ui like the rest of the chrome. */
.card.frame { --fpx: calc(1px * var(--fui, var(--ui))); }
.frame-label, .cards.lod .frame-label {
  top: calc(-34 * var(--fpx)); left: 0; height: calc(28 * var(--fpx));
  width: max-content; max-width: 100%; min-width: 0;
  gap: calc(7 * var(--fpx)); padding: 0 calc(7 * var(--fpx)); border-radius: calc(5 * var(--fpx));
  font-size: calc(12 * var(--fpx)); font-weight: var(--fw-medium);
}
/* Too small on screen to carry a name: the name gives way rather than shrink to "R…". */
.card.frame.name-off .frame-label { visibility: hidden; }
/* The name is part of the selection: accent outline and accent name read as one
   object. A frame about to take dragged cards names itself the same way, so what
   you are dropping into is said in words as well as by the dashed edge. */
.card.frame.selected .frame-label, .card.frame.drop-into .frame-label { background: var(--state-selected); color: var(--accent-text); }
.card.frame.selected .frame-count, .card.frame.drop-into .frame-count { color: inherit; }
.frame-label .ti { flex-shrink: 0; opacity: .7; }
.frame-label .card-title { min-width: 0; color: inherit; text-overflow: ellipsis; mask-image: none; }
.frame-count {
  flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
  min-width: calc(18 * var(--fpx)); height: calc(18 * var(--fpx));
  padding: 0 calc(5 * var(--fpx)); border-radius: calc(5 * var(--fpx));
  background: var(--fill-subtle); color: var(--text-secondary);
  font-size: calc(10 * var(--fpx)); font-variant-numeric: tabular-nums;
}
.frame-count:empty { display: none; }
.card.text:not(.untitled) .card-head {
  height: auto; min-height: 46px; padding: 16px 18px 7px; align-items: flex-start;
  font-size: 16px; line-height: 1.4; font-weight: var(--fw-semibold);
}
.card.text .card-title {
  min-width: 0; white-space: normal; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  mask-image: none !important;
}
.card.text .rename-input { margin-right: 0; width: 100%; font: inherit; line-height: 1.4; padding: 0 5px; }
.frame-label .rename-input { font: inherit; line-height: 1.4; min-width: calc(160 * var(--fpx)); }
/* Typing a name is not bound by the frame's width: a narrow region's field used to
   show eight characters of what was being typed. */
.frame-label:has(.rename-input) { max-width: none; }

/* Text stays on the board; wheel gestures belong to the canvas. */
.card.text .text-body { overflow: clip; }

/* A region is a grouping wash, never an elevated card. */
.card.frame, .card.frame:hover, .card.frame.selected, .card.frame.selected:hover { box-shadow: none; }
/* Quiet indicators do not need the same depth as a working popover. */
.tip, .canvas-hint, .minimap, .present-exit, .offscreen-beacon { box-shadow: var(--shadow-sm); }
.peek { box-shadow: -3px 0 14px var(--shadow-contact); }
.rz-handle, .empty-art span { box-shadow: none; }

.card:not(.frame).lifted { box-shadow: var(--card-elev-drag); }
/* A frame moves as a plane: the region and what it carries slide at rest size. The
   lift scale grew each card about its own centre (the gaps between them changed and
   the frame's name shifted), and every carried card got a compositor layer and a
   scale/shadow transition at pick-up and again at drop. The drag shadow still says
   which cards are coming along. */
.card.lifted.carried { scale: none; will-change: auto; }
.card.carried { transition: none; } /* kept two frames past the drop (app.js putDown) */
/* What a frame being drawn will hold: the quiet version of selection (.ol-hover). */
.card.joining:not(.selected) { outline-color: color-mix(in oklch, var(--accent) 55%, transparent); }

.outline-foot { border-top: 0; }

/* SVG viewBox scaling and the canvas scale cancel each other for header icons.
   A non-scaling SVG stroke bypasses the first step and becomes bold at high zoom. */
.world .card-head svg[viewBox="0 0 24 24"] * { vector-effect: none; stroke-width: 1.875; }
/* Only actions duplicated by the selected-card toolbar step out. Keyboard focus
   and live browsing keep the header's direct actions available. */
/* .tb-solo: the toolbar is out for one card (app.js syncToolbarSolo), set from script
   rather than asked of the whole app with :has(). */
.app.tb-solo .card.web.selected:not(.interacting) .card-head-actions:not(:focus-within) { opacity: 0; pointer-events: none; }
.app.tb-solo .card.web.selected:not(.interacting):not(:focus-within) > .card-head > .card-title { mask-image: none; text-overflow: ellipsis; }

/* Interaction states: stable geometry, transient neutral feedback, persistent selection.
   These apply to controls, never the canvas card or frame surface. */
.app :is(.board-name, .account-switch, .side-item, .ol-back, .zoom-pct, .add-btn,
  .peek-tab, .peek-tab-close, .expand-collapse, .live-done, .recent, .tpl, .link-btn) {
  transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.app :is(.board-name, .account-switch, .side-item, .ol-back, .zoom-pct, .recent,
  .site, .note-item, .peek-src, .offscreen-beacon, .peek-tab-close):not(:disabled):active,
.ol-item:not(.on):active, .peek-tab:not(.active):active, .pl-item:active {
  background-color: var(--state-active);
}
.app :is(.icon-btn, .card-toolbar button, .link-toolbar button, .add-btn, .board-name, .account-switch)[aria-expanded="true"] {
  background-color: var(--state-selected); color: var(--text-primary);
}
.app :is(.icon-btn, .card-toolbar > button, .link-toolbar > button)[aria-pressed="true"] {
  background-color: var(--state-selected); color: var(--text-primary);
}
.app :is(.icon-btn.on, .ol-item.on, .link-toolbar button.on, .card-toolbar button[aria-expanded="true"],
  .icon-btn[aria-expanded="true"], .icon-btn[aria-pressed="true"], .card-toolbar > button[aria-pressed="true"],
  .link-toolbar > button[aria-pressed="true"], .add-btn.open, .board-name.open, .account-switch[aria-expanded="true"]):hover {
  background-color: var(--state-selected-hover);
}
.app :is(.icon-btn.on, .ol-item.on, .link-toolbar button.on, .card-toolbar button[aria-expanded="true"],
  .icon-btn[aria-expanded="true"], .icon-btn[aria-pressed="true"], .card-toolbar > button[aria-pressed="true"],
  .link-toolbar > button[aria-pressed="true"], .add-btn.open, .board-name.open, .account-switch[aria-expanded="true"]):active {
  background-color: var(--state-selected-active);
}
.peek-tab.active:hover { background-color: var(--control-thumb); }
.live-done:active { background: var(--accent-active); }
.peek-pin-btn:not(.on-canvas):active { background: var(--action-tonal-bg-hover); }
.peek-newtab form button:not(:disabled):active { background: var(--accent-active); }
.app button:not(:disabled):active { transition-duration: var(--dur-instant); }
.app button:disabled { opacity: var(--state-disabled-opacity); transform: none; filter: none; }
.app :is(.icon-btn, .popover button, .ctx-menu button):disabled:is(:hover, :active) { background: transparent; color: var(--text-secondary); }
.world :is(button, .frame-label):focus-visible { outline-width: calc(2 * var(--px)); outline-offset: calc(2 * var(--px)); }
.link-label-in:focus-visible { outline: calc(2 * var(--px)) solid var(--accent-line); outline-offset: calc(2 * var(--px)); }

/* Theme/segmented choices keep their selected surface under the pointer. */
.popover .seg-text button[aria-pressed="true"]:hover { background: var(--control-thumb); }
.popover .seg-text button[aria-pressed="true"]:active { background: var(--state-selected-active); }

/* Cursor expresses the next action; a held gesture keeps its cursor until release. */
.card.text.editing .text-body, .card.text.editing .text-body * { cursor: text; }
.card.text.editing .text-body a { cursor: text; }
input, textarea, [contenteditable="true"], .rename-input { cursor: text; }
input:disabled, textarea:disabled, button:disabled, [aria-disabled="true"] { cursor: default; }
.ol-item, .pl-item, .note-item { cursor: pointer; }
.viewport[data-tool="text"] { cursor: text; }
.viewport[data-tool="web"] { cursor: crosshair; }
.viewport[data-tool="line"] .world, .viewport[data-tool="line"] .world * { cursor: crosshair; }
.viewport:is([data-tool="hand"], .space) .world,
.viewport:is([data-tool="hand"], .space) .world * { cursor: grab; }
.viewport:is([data-tool="hand"], .space) .world button { cursor: pointer; }
.viewport:is([data-tool="hand"], .space) .world button:disabled { cursor: default; }
.viewport:is([data-tool="hand"], .space) .world :is(input, textarea, [contenteditable="true"], [contenteditable="true"] *) { cursor: text; }
.app.present .world, .app.present .world * { cursor: grab; }
.app.present .card.interacting .card-body { cursor: auto; }
.app.present .world button { cursor: pointer; }
/* A held gesture (pan, drag, resize, connect, marquee, draw) keeps its cursor on this
   transparent layer over the board, which app.js shows once the pointer has travelled
   (holdGestureCursor). Not `.viewport.dragging *` and the like: a selector ending in `*`
   restyles every node on the board each time its class goes on or off. Under the
   toolbars and Side Peek, which the macOS app hit-tests (desktop.js onTop). */
.gesture-shield { position: absolute; inset: 0; z-index: 15; }

/* ---------- diagrams: shapes, mind-map topics, branches ---------- */
/* A shape is a flat diagram object: a fill and a line in its colour, text in the
   middle. Its edge is the card ring (--card-ring), so hover, selection and lifting
   keep working exactly as they do for notes. Diamonds and parallelograms are a
   stretched path instead, and the ring steps aside for it. */
.card.shape {
  --shape-fill: var(--note-dark-bg); --shape-line: var(--note-dark-solid); --shape-on-solid: var(--note-dark-on-solid);
  --card-shadow: 0 0 0 transparent; --card-shadow-hover: var(--card-shadow); --card-top: inset 0 0 0 transparent;
  --card-ring: inset 0 0 0 calc(1.5 * var(--px)) var(--shape-line);
  background: var(--shape-fill); overflow: visible; justify-content: center; border-radius: var(--r-shape);
}
.card.shape.color-blue { --shape-fill: var(--note-blue-bg); --shape-line: var(--note-blue-solid); --shape-on-solid: var(--note-blue-on-solid); }
.card.shape.color-teal { --shape-fill: var(--note-teal-bg); --shape-line: var(--note-teal-solid); --shape-on-solid: var(--note-teal-on-solid); }
.card.shape.color-amber { --shape-fill: var(--note-amber-bg); --shape-line: var(--note-amber-solid); --shape-on-solid: var(--note-amber-on-solid); }
.card.shape.color-rose { --shape-fill: var(--note-rose-bg); --shape-line: var(--note-rose-solid); --shape-on-solid: var(--note-rose-on-solid); }
.card.shape.s-pill { border-radius: 999px; }
.card.shape.s-ellipse { border-radius: 50%; }
.card.shape.s-diamond, .card.shape.s-para, .card.shape.s-doc, .card.shape.s-cyl { background: transparent; --card-ring: inset 0 0 0 transparent; }
.card.shape .shape-bg path.rim { fill: none; }
.card.shape.s-doc .shape-text { padding: 10px 16px 13%; }
.card.shape.s-cyl .shape-text { padding: 20% 14% 9%; }
.card.shape .shape-bg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.card.shape .shape-bg path { fill: var(--shape-fill); stroke: var(--shape-line); stroke-width: 1.5; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.card.shape .shape-text {
  position: relative; flex: none; max-height: 100%; overflow: hidden; outline: 0; cursor: default;
  padding: 10px 16px; text-align: center; color: var(--text-primary);
  font-size: var(--fs-shape); line-height: var(--lh-snug); font-weight: var(--fw-medium); letter-spacing: var(--ls-shape); overflow-wrap: anywhere;
}
.card.shape.s-ellipse .shape-text { padding: 12% 16%; }
.card.shape.s-diamond .shape-text { padding: 20% 25%; }
.card.shape.s-para .shape-text { padding: 10px 20%; }
.card.shape.s-pill .shape-text { padding: 10px 22px; }
.card.shape.editing { outline-color: var(--accent); outline-width: calc(2 * var(--px)); outline-offset: calc(2 * var(--px)); }
.card.shape.editing .shape-text { cursor: text; overflow: visible; }
.card.shape.editing .shape-text:empty::before { content: "Type…"; color: var(--text-secondary); }
/* While typing, the keys that matter sit under the shape. */
.card.shape .shape-hint {
  display: none; position: absolute; left: 50%; top: 100%; z-index: 2; pointer-events: none; white-space: nowrap;
  transform: translate(-50%, calc(8 * var(--px)));
  padding: calc(3 * var(--px)) calc(8 * var(--px)); border-radius: calc(6 * var(--px));
  background: var(--surface-overlay); box-shadow: var(--shadow-md); color: var(--text-secondary);
  font-size: calc(11 * var(--px)); font-weight: var(--fw-medium);
}
.card.shape.editing .shape-hint:not(:empty) { display: block; animation: fade-in var(--dur-base); }

/* Topics: the root is solid, a main branch is a tinted box in its branch colour, and
   everything further out is quieter still, so the eye reads the map from the middle. */
.card.shape.s-topic { border-radius: var(--r-shape); }
.card.shape.s-topic .shape-text { padding: 8px 14px; font-size: var(--fs-shape); white-space: pre-wrap; }
.card.shape.in-map.lvl-0 { border-radius: var(--r-map-root); --card-shadow: var(--shadow-map-root); --card-ring: inset 0 0 0 0 transparent; background: var(--text-primary); }
.card.shape.in-map.lvl-0 .shape-text { padding: 14px 24px; font-size: var(--fs-map-root); font-weight: var(--fw-semibold); color: var(--surface-raised); letter-spacing: var(--ls-map-root); }
.card.shape.in-map.lvl-0:not(.color-dark) { background: var(--shape-line); }
.card.shape.in-map.lvl-0:not(.color-dark) .shape-text { color: var(--shape-on-solid); }
.card.shape.in-map.lvl-1 { --card-ring: inset 0 0 0 calc(2 * var(--px)) var(--shape-line); }
.card.shape.in-map.lvl-1 .shape-text { padding: 9px 16px; font-size: var(--fs-map-branch); font-weight: var(--fw-semibold); }
.card.shape.in-map.lvl-2 { border-radius: var(--r-map-leaf); --card-ring: inset 0 0 0 calc(1 * var(--px)) color-mix(in oklab, var(--shape-line) 45%, transparent); }
.card.shape.in-map.lvl-2 .shape-text { padding: 6px 12px; font-size: var(--fs-map-leaf); }
.card.shape.in-map .rz { display: none; }
.card.shape.in-map.lvl-0.editing .shape-text:empty::before { content: "Central topic"; color: inherit; opacity: 0.55; }
.card.shape.in-map.editing .shape-text:empty::before { content: "Topic"; }
/* An input method composing: the box grows down round the provisional text (app.js
   holds the map, or the shape's size, still until the text is committed). Size containment would keep an
   auto height at zero. */
.card.shape.editing.composing { height: auto !important; container-type: normal; }
.card.map-hidden { display: none !important; }
.card.shape.shape-measure { position: fixed; left: -100000px; top: 0; height: auto; contain: none; container-type: normal; transform: none; visibility: hidden; pointer-events: none; --px: 1px; animation: none; }
.card.shape.shape-measure.in-map { width: max-content; max-width: 240px; }
.card.shape.shape-measure.in-map.lvl-0 { max-width: 320px; }
.card.shape.shape-measure.in-map.lvl-1 { max-width: 260px; }

/* Branches belong to the map, not to the pointer: drawn in world units, so they are
   heavier near the root and thin out toward the leaves. */
.links .branches { pointer-events: none; }
.links .branch { fill: none; stroke: var(--link-stroke); stroke-linecap: round; stroke-linejoin: round; stroke-width: 2; }
.links .branch.d1 { stroke-width: 3; }
.links .branch.d2 { stroke-width: 2; }
.links .branch.d3 { stroke-width: 1.5; }
.links .branch.c-blue { stroke: var(--note-blue-solid); } .links .branch.c-teal { stroke: var(--note-teal-solid); }
.links .branch.c-amber { stroke: var(--note-amber-solid); } .links .branch.c-rose { stroke: var(--note-rose-solid); }
.app.interacting .links .branch { opacity: 0.72; }
/* While a topic is dragged onto another, its branch runs from that topic: a proposal. */
.links .branch.drop-preview { opacity: 0.55; }

/* + and fold handles on a topic; screen-constant, like the connection dots. */
.map-add, .map-fold { position: absolute; left: 0; top: 0; width: 32px; height: 32px; margin: -16px; padding: 0; background: none; border: 0; border-radius: 50%; pointer-events: auto; transform-origin: 16px 16px; animation: fade-in var(--dur-fast); }
.map-add > span, .map-fold > span { position: absolute; left: 50%; top: 50%; display: grid; place-items: center; cursor: pointer; }
.map-add > span { width: 20px; height: 20px; margin: -10px 0 0 -10px; border-radius: 50%; background: var(--surface-raised); color: var(--text-secondary); box-shadow: 0 0 0 1px var(--border-strong), 0 1px 3px oklch(0% 0 0 / 0.18); transition: background var(--dur-fast), color var(--dur-fast), transform var(--dur-fast) var(--ease-spring); }
.map-add > span svg { width: var(--icon-xs); height: var(--icon-xs); }
.map-add:hover > span { background: var(--accent); color: var(--text-on-accent); box-shadow: 0 0 0 1px var(--accent); transform: scale(1.12); }
.map-add:focus-visible > span, .map-fold:focus-visible > span { outline: 2px solid var(--accent-line); outline-offset: 3px; }
.map-fold { --fold-line: var(--link-stroke); --fold-ink: var(--text-on-link-solid); }
.map-fold.c-blue { --fold-line: var(--note-blue-solid); --fold-ink: var(--note-blue-on-solid); } .map-fold.c-teal { --fold-line: var(--note-teal-solid); --fold-ink: var(--note-teal-on-solid); } .map-fold.c-amber { --fold-line: var(--note-amber-solid); --fold-ink: var(--note-amber-on-solid); } .map-fold.c-rose { --fold-line: var(--note-rose-solid); --fold-ink: var(--note-rose-on-solid); }
.map-fold > span { min-width: 16px; height: 16px; margin: -8px 0 0 -8px; padding: 0 3px; border-radius: 8px; background: var(--surface-raised); color: var(--text-secondary); box-shadow: 0 0 0 1.5px var(--fold-line); font-size: 10px; font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; line-height: 1; }
.map-fold > span svg { width: 10px; height: 10px; }
.map-fold.folded > span { min-width: 20px; height: 18px; margin: -9px 0 0 -6px; padding: 0 5px; border-radius: 9px; background: var(--fold-line); color: var(--fold-ink); box-shadow: none; }
.map-fold:hover > span { box-shadow: 0 0 0 1.5px var(--accent); color: var(--text-primary); }
.map-fold.folded:hover > span { background: var(--accent); color: var(--text-on-accent); }
/* A dot that also adds the next card says so under the pointer: a + in the accent dot,
   as FigJam's and Miro's quick-create handles do. */
.anchor.quick:hover span { transform: scale(2); border-color: var(--accent); opacity: 1;
  background: linear-gradient(var(--text-on-accent), var(--text-on-accent)) center / 4.2px 0.9px no-repeat, linear-gradient(var(--text-on-accent), var(--text-on-accent)) center / 0.9px 4.2px no-repeat, var(--accent); }

/* Template thumbnails for diagrams */
.tpl-preview .tp-shape { fill: var(--note-dark-bg); stroke: var(--note-dark-solid); stroke-width: 1; vector-effect: non-scaling-stroke; }
.tpl-preview .tp-shape.tp-s-blue { fill: var(--note-blue-bg); stroke: var(--note-blue-solid); } .tpl-preview .tp-shape.tp-s-teal { fill: var(--note-teal-bg); stroke: var(--note-teal-solid); }
.tpl-preview .tp-shape.tp-s-amber { fill: var(--note-amber-bg); stroke: var(--note-amber-solid); } .tpl-preview .tp-shape.tp-s-rose { fill: var(--note-rose-bg); stroke: var(--note-rose-solid); }
.tpl-preview .tp-shape.tp-root { fill: var(--text-primary); stroke: none; }
.tpl-preview .tp-branch { fill: none; stroke: var(--link-stroke); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.tpl-preview .tp-branch.tp-s-blue { stroke: var(--note-blue-solid); } .tpl-preview .tp-branch.tp-s-teal { stroke: var(--note-teal-solid); }
.tpl-preview .tp-branch.tp-s-amber { stroke: var(--note-amber-solid); } .tpl-preview .tp-branch.tp-s-rose { stroke: var(--note-rose-solid); }
.tpl-preview path.tp-link { fill: none; vector-effect: non-scaling-stroke; }
.pop-item .pi .note-color-dot, [role="menuitem"] .pi .note-color-dot { display: block; width: 14px; height: 14px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgb(255 255 255 / 12%); }
/* A shape's border is its ring, so the undo flash keeps it while the halo spreads. */
@keyframes shape-flash { 0% { box-shadow: var(--card-ring), 0 0 0 0 var(--accent-ring); } 100% { box-shadow: var(--card-ring), 0 0 0 18px transparent; } }
.card.shape.flash { animation: shape-flash var(--dur-pulse) var(--ease-out); }
@media (pointer: coarse) {
  .map-add, .map-fold { width: 44px; height: 44px; margin: -22px; transform-origin: 22px 22px; }
  .map-add > span { width: 28px; height: 28px; margin: -14px 0 0 -14px; }
  .map-add > span svg { width: var(--icon-md); height: var(--icon-md); }
  .map-fold > span { min-width: 22px; height: 22px; margin: -11px 0 0 -11px; border-radius: 11px; font-size: 11px; }
}

/* The root uses --shadow-map-root; all other diagram objects stay flat. */
/* Tapered branches are filled shapes; plain ones (logic chart, tree) are strokes. */
.links .branch { --branch: var(--link-stroke); stroke: var(--branch); }
.links .branch.c-blue { --branch: var(--note-blue-solid); } .links .branch.c-teal { --branch: var(--note-teal-solid); }
.links .branch.c-amber { --branch: var(--note-amber-solid); } .links .branch.c-rose { --branch: var(--note-rose-solid); }
.links .branch.tapered { fill: var(--branch); stroke: none; }

/* A source: the cited site's icon on a small tile — inside a topic, at its end; on a
   free shape, as a badge on its corner. It opens the page in Side Peek. */
.card.shape .src-chip {
  position: absolute; z-index: 3; display: grid; place-items: center;
  width: calc(22 * var(--px)); height: calc(22 * var(--px)); min-width: 18px; min-height: 18px; max-width: 30px; max-height: 30px; padding: 0;
  border-radius: calc(var(--source-chip-radius) * var(--px)); background: var(--surface-raised); cursor: pointer;
  box-shadow: 0 0 0 1px color-mix(in oklab, var(--shape-line) 55%, transparent), var(--elev-1);
  transition: transform var(--dur-fast) var(--ease-spring), box-shadow var(--dur-fast);
}
.card.shape .src-chip .fav { width: 62%; height: 62%; border-radius: 3px; pointer-events: none; }
.card.shape .src-chip:hover { transform: scale(1.12); box-shadow: 0 0 0 1.5px var(--accent), var(--elev-2); }
.card.shape:not(.in-map) .src-chip { top: 0; right: 0; translate: 35% -35%; }
.card.shape.in-map .src-chip { right: 8px; top: 50%; translate: 0 -50%; width: 20px; height: 20px; min-width: 0; min-height: 0; border-radius: var(--r-source-chip); }
.card.shape.in-map.lvl-0 .src-chip { right: 12px; width: 24px; height: 24px; }
.card.shape.in-map.has-src .shape-text { padding-right: 36px; }
.card.shape.in-map.lvl-0.has-src .shape-text { padding-right: 46px; }
.card.shape.editing .src-chip { pointer-events: none; opacity: 0.5; }
.card-toolbar .tb-fav { width: 16px; height: 16px; border-radius: 3px; }

/* Side Peek: "Add to map" sits beside "Pin to board" while a topic is selected. */
.peek-map-btn { display: flex; align-items: center; gap: 6px; height: var(--ctl-sm); padding: 0 10px 0 var(--sp-2); border-radius: var(--r-sm); background: var(--action-tonal-bg); color: var(--action-tonal-fg); font-weight: var(--fw-semibold); font-size: var(--fs-sm); letter-spacing: var(--ls-12); white-space: nowrap; transition: background var(--dur-fast); }
.peek-map-btn:hover { background: var(--action-tonal-bg-hover); }
.peek-map-btn:disabled { background: none; color: var(--text-tertiary); cursor: default; }
.peek-map-btn .pi svg { width: var(--icon-md); height: var(--icon-md); }
.peek-foot .peek-map-btn:not([hidden]) { margin-left: auto; }
.peek-foot .peek-map-btn:not([hidden]) + .peek-pin-btn { margin-left: 0; }

/* ---------- web clips: a passage, a picture, a video, a screenshot, with its source ---------- */
/* A clip is content, so it sits on the content plane like a note; what makes it a clip
   is the source line along its foot. A quote is set in the serif and marked with a
   highlighter stroke — the amber content tone, never the accent, which stays reserved
   for what the user caused. A picture fills its card edge to edge. */
.card.clip { background: var(--surface-raised); }
.card.clip .clip-quote { position: relative; flex: 1; min-height: 0; margin: 0; padding: 14px 18px 10px 24px; overflow: hidden; }
.card.clip .clip-quote::before { content: ''; position: absolute; left: 11px; top: 16px; bottom: 12px; width: 3px; border-radius: 2px; background: var(--tone-amber-9); }
.card.clip .q-text { margin: 0; font-family: var(--font-serif); font-size: var(--fs-note); line-height: var(--lh-note); letter-spacing: 0; color: var(--text-primary); white-space: pre-wrap; overflow-wrap: anywhere; }
.card.clip .clip-media { position: relative; flex: 1; min-height: 0; overflow: hidden; background: var(--fill-subtle); }
.card.clip .clip-img { display: block; width: 100%; height: 100%; object-fit: cover; pointer-events: none; user-select: none; -webkit-user-drag: none; }
.card.clip.k-shot .clip-media { box-shadow: inset 0 -1px 0 var(--border-subtle); }
.card.clip .clip-noposter { position: absolute; inset: 0; background: linear-gradient(135deg, var(--tone-blue-3, var(--fill-subtle)), var(--fill-subtle)); }
.card.clip.broken .clip-img { visibility: hidden; }
.card.clip.k-image.broken .clip-media::after, .card.clip.k-shot.broken .clip-media::after { content: 'Picture unavailable'; position: absolute; inset: 0; display: grid; place-items: center; color: var(--text-tertiary); font-size: var(--fs-sm); }
.card.clip .clip-play { position: absolute; left: 50%; top: 50%; width: 52px; height: 52px; margin: -26px 0 0 -26px; border-radius: 50%; display: grid; place-items: center; background: oklch(0% 0 0 / 0.55); color: oklch(100% 0 0); box-shadow: 0 2px 12px oklch(0% 0 0 / 0.3); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); transition: transform var(--dur-fast) var(--ease-out), background var(--dur-fast); }
.card.clip .clip-play svg { width: 22px; height: 22px; margin-left: 3px; fill: currentColor; }
.card.clip .clip-play:hover { transform: scale(1.06); background: oklch(0% 0 0 / 0.7); }
.card.clip.playing .clip-media { background: oklch(0% 0 0); }
.card.clip .clip-player { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* Source is a footnote: a small site mark and one domain. Full provenance lives
   in the source preview; the quiet line keeps a generous horizontal hit area. */
:is(.card.clip, .card.table) .clip-source-row { flex: none; display: flex; align-items: center; height: 28px; }
:is(.card.clip, .card.table) .clip-src { flex: 1; height: 28px; min-width: 0; display: flex; align-items: center; gap: 6px; padding: 0 12px; text-align: left; color: var(--text-secondary); font-size: var(--fs-xs); font-weight: var(--fw-regular); letter-spacing: var(--ls-11); transition: background var(--dur-fast), color var(--dur-fast); }
:is(.card.clip, .card.table) .clip-src .fav { width: 12px; height: 12px; flex: none; border-radius: 2px; }
:is(.card.clip, .card.table) .clip-src .cs-title { min-width: 0; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clip-src .cs-site { position: relative; display: grid; place-items: center; width: 12px; height: 12px; flex: none; transition: scale var(--dur-fast) var(--ease-out); }
.clip-src .cs-site > svg { width: 12px; height: 12px; color: var(--text-secondary); }
.clip-src .cs-site .fav { position: absolute; inset: 0; filter: grayscale(1); opacity: 0.8; transition: filter var(--dur-fast), opacity var(--dur-fast); }
.clip-src:is(:hover, :focus-visible, [aria-pressed="true"]) .fav { filter: none; opacity: 1; }
.clip-src:active .cs-site { scale: 0.92; }
:is(.card.clip, .card.table, .clip-viewer) .clip-src:is(:hover, :focus-visible) { background: var(--state-hover); color: var(--text-primary); }
.clip-src[aria-pressed="true"] { color: var(--action-tonal-fg); background: var(--action-tonal-bg); }
.clip-details { flex: none; display: grid; place-items: center; width: 28px; height: 28px; margin-right: 4px; border-radius: var(--r-xs); color: var(--text-secondary); opacity: 0; translate: -2px 0; pointer-events: none; transition: opacity var(--dur-fast), translate var(--dur-fast) var(--ease-out), background var(--dur-fast); }
.clip-details svg { width: 14px; height: 14px; }
.clip-source-row:is(:hover, :focus-within) .clip-details, .clip-details[aria-expanded="true"] { opacity: 1; translate: 0; pointer-events: auto; }
.clip-details:hover { color: var(--text-primary); background: var(--state-hover); }
.clip-details:active { scale: 0.94; }
:is(.card.clip, .card.table).peek-source { outline-style: solid; }
:is(.card.clip, .card.table).peek-source:not(.selected) { outline-color: var(--border-default); }
:is(.card.clip, .card.table) .clip-src:focus-visible { outline-offset: -3px; border-radius: var(--r-xs); }
@media (pointer: coarse) {
  :is(.card.clip, .card.table, .clip-viewer) :is(.clip-source-row, .clip-src, .clip-details) { height: 44px; }
  .clip-details { width: 44px; opacity: 1; translate: 0; pointer-events: auto; }
}
.card.clip.clip-measure { position: fixed; left: -100000px; top: 0; height: auto; contain: none; container-type: normal; transform: none; visibility: hidden; pointer-events: none; animation: none; --px: 1px; }
.clip-measure .clip-quote { flex: none; overflow: visible; }
/* Zoomed out a quote is a title like every other card; a picture stays a picture. */
.cards.lod .card.clip.k-quote .clip-quote, .cards.lod .card.clip.k-quote .clip-src { opacity: 0; pointer-events: none; }
.cards.lod .card.clip .lod-cover { background: var(--surface-raised); color: var(--text-primary); }
.cards.lod .card.clip:not(.k-quote) .clip-src { pointer-events: none; }
.cards.lod .clip-source-row { opacity: 0; pointer-events: none; }
/* A picture full size, over everything; the source line comes along. */
.clip-viewer { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: 40px 40px 88px; background: oklch(0% 0 0 / 0.8); animation: fade-in var(--dur-fast); }
.clip-viewer > img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: var(--r-md); box-shadow: 0 8px 40px oklch(0% 0 0 / 0.5); background: var(--surface-raised); animation: clip-image-in var(--dur-base) var(--ease-out); transition: opacity var(--dur-fast), translate var(--dur-fast), scale var(--dur-fast); }
@keyframes clip-image-in { from { opacity: 0; translate: 0 6px; scale: 0.99; } }
.clip-viewer.closing { animation: none; opacity: 0; transition: opacity var(--dur-fast); pointer-events: none; }
.clip-viewer.closing > img { animation: none; opacity: 0; translate: 0 4px; scale: 0.99; }
.clip-viewer .cv-bar { position: absolute; left: 50%; bottom: 24px; transform: translateX(-50%); max-width: min(640px, calc(100vw - 32px)); display: flex; align-items: center; gap: 4px; padding: 4px; border-radius: var(--r-md); background: var(--surface-overlay); border: 1px solid var(--overlay-border); box-shadow: var(--shadow-lg); }
.clip-viewer .clip-source-row { display: flex; align-items: center; min-width: 0; }
.clip-viewer .clip-src { flex: 1; display: flex; align-items: center; gap: 6px; min-width: 0; height: 32px; padding: 0 10px; border-radius: var(--r-sm); color: var(--text-secondary); font-size: var(--fs-xs); }
.clip-viewer .clip-details { opacity: 1; translate: 0; pointer-events: auto; }
.clip-viewer .clip-src:hover { background: var(--state-hover); color: var(--text-primary); }
.clip-viewer .clip-src .fav { width: 12px; height: 12px; flex: none; border-radius: 2px; }
.clip-viewer .clip-src .cs-title { min-width: 0; max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (pointer: coarse) { .clip-viewer .clip-src { height: 44px; } }
/* Side Peek's footer: a screenshot of the page being read (the Mac app). */
.peek-shot-btn { display: grid; place-items: center; width: var(--ctl-sm); height: var(--ctl-sm); border-radius: var(--r-sm); color: var(--text-secondary); transition: background var(--dur-fast), color var(--dur-fast); }
.peek-shot-btn:hover { background: var(--state-hover); color: var(--text-primary); }
.peek-shot-btn .pi svg { width: var(--icon-md); height: var(--icon-md); }
.peek-foot .peek-shot-btn:not([hidden]) { margin-left: auto; }
.peek-foot .peek-shot-btn:not([hidden]) ~ .peek-map-btn, .peek-foot .peek-shot-btn:not([hidden]) ~ .peek-pin-btn { margin-left: 0; }

/* ---------- fishbone and timeline: the spine or axis is the strongest line ---------- */
/* Bones and moments keep their branch colours; the line that holds them together is
   neutral and heavier, so the eye reads the structure first and the branches second. */
.links .branch.spine { stroke: var(--text-tertiary); stroke-width: 3.5; }
.links .branch.spine.timeline { stroke-width: 2.5; }
.tpl-preview .tp-branch.tp-spine { stroke: var(--text-tertiary); stroke-width: 2.5; }

/* ---------- tables ---------- */
/* A table is content on the content plane: rules only between cells (the card's own
   edge is the outer one), a header row one fill step down, as tall as its rows. The
   cell being typed in carries the accent ring; that is the only accent on a table. */
.card.table { background: var(--surface-raised); }
.card.table .tbl-cap { display: none; flex: none; padding: 10px 12px 6px; font-size: var(--fs-md); font-weight: var(--fw-semibold); color: var(--text-primary); cursor: grab; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card.table.titled .tbl-cap { display: block; }
.card.table .tbl-cap .rename-input { width: 100%; }
.card.table .tbl-wrap { position: relative; flex: none; }
.card.table .tbl { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: var(--fs-md); line-height: 1.45; color: var(--text-primary); }
.card.table.titled .tbl { border-top: 1px solid var(--border-subtle); }
.card.table .tbl th, .card.table .tbl td { padding: 0; border-right: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle); vertical-align: top; text-align: left; font-weight: var(--fw-regular); }
.card.table .tbl tr > :last-child { border-right: 0; }
.card.table .tbl tr:last-child > * { border-bottom: 0; }
.card.table .tbl th { background: var(--fill-subtle); font-weight: var(--fw-semibold); }
.card.table .cell { min-height: 33px; padding: 7px 10px; overflow-wrap: anywhere; white-space: normal; outline: none; }
.card.table .cell.on { white-space: pre-wrap; background: var(--surface-raised); box-shadow: inset 0 0 0 2px var(--accent); cursor: text; }
.card.table.editing { cursor: default; }
.card.table .cell-link { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; color: var(--accent-text, var(--accent)); text-decoration: underline; text-decoration-color: color-mix(in oklch, currentColor 35%, transparent); text-underline-offset: 2px; cursor: pointer; }
.card.table .cell-link .fav { width: 12px; height: 12px; margin-right: 4px; vertical-align: -1px; border-radius: 2px; }
.card.table .cell-link:hover { text-decoration-color: currentColor; }
/* Column edges: grips only on a selected table, so dragging a table never resizes it. */
.card.table .col-rz { position: absolute; top: 0; bottom: 0; width: 9px; margin-left: -4px; cursor: col-resize; z-index: 2; }
.card.table:not(.selected) .col-rz, .card.table.editing .col-rz { pointer-events: none; }
.card.table.selected .col-rz:hover::after { content: ''; position: absolute; left: 4px; top: 0; bottom: 0; width: 1px; background: var(--accent); }
.card.table.table-measure { position: fixed; left: -100000px; top: 0; height: auto; contain: none; container-type: normal; transform: none; visibility: hidden; pointer-events: none; animation: none; --px: 1px; }
.cards.lod .card.table .tbl-wrap, .cards.lod .card.table .tbl-cap, .cards.lod .card.table .clip-src { opacity: 0; pointer-events: none; }
.cards.lod .card.table .lod-cover { background: var(--surface-raised); color: var(--text-primary); }
.tpl-preview .tp-grid { fill: var(--surface-raised); stroke: none; }
.tpl-preview .tp-grid.tp-edge { fill: none; stroke: var(--border-strong); stroke-width: 1.25; vector-effect: non-scaling-stroke; }
.tpl-preview path.tp-head { fill: var(--fill-strong); stroke: none; }
.tpl-preview .tp-rule { fill: none; stroke: var(--border-default); stroke-width: 1; vector-effect: non-scaling-stroke; }
.tpl-preview .tp-lane { fill: var(--frame-fill, var(--fill-subtle)); stroke: none; }

/* ---------- view transitions ----------
   An appearance change crossfades the old picture into the new one (applyTheme starts
   the transition). Between pages it does not: with `@view-transition { navigation: auto }`
   Chromium aborted most cross-document transitions here — the home page into /app, one
   board into the next in the Mac app — with an uncaught "opt-in disabled" error, and a
   fade that runs one time in ten reads as a glitch. */
::view-transition-old(root), ::view-transition-new(root) { animation-duration: var(--dur-slow); animation-timing-function: var(--ease-in-out); }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
  .card.flash { animation: none; box-shadow: 0 0 0 6px var(--accent-ring), var(--shadow-card); }
  .card.shape.flash { animation: none; box-shadow: var(--card-ring), 0 0 0 6px var(--accent-ring); }
}
/* Display identities: character artwork and background remain separate. */
.identity-face{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:var(--identity-size,36px);height:var(--identity-size,36px);border-radius:50%;overflow:hidden;background:var(--surface-raised,#e3e5e0);color:var(--text-primary);font-weight:600}
.identity-face img,.comment-avatar img,.collab-avatar img{display:block;width:100%;height:100%;object-fit:contain;border-radius:inherit}
.identity-row{display:flex;align-items:center;gap:12px;padding:14px 0;border-bottom:1px solid var(--border-default);margin-bottom:12px}
.identity-row>div{flex:1;min-width:0}.identity-row strong,.identity-row small{display:block}.identity-row small{font-size:12px;color:var(--text-tertiary);margin-top:3px}
/* Identity artwork remains circular; its surrounding editor uses native-dialog. */
.identity-dialog label { display:block; font-size:var(--fs-md); }
.identity-dialog input:not([type=color]) { margin-top:var(--sp-2); }
.identity-dialog fieldset { border:0; padding:0; margin:var(--sp-5) 0; min-width:0; }
.identity-dialog legend { font-size:var(--fs-md); font-weight:var(--fw-medium); margin-bottom:var(--sp-2); }
.identity-preview { display:flex; align-items:center; gap:var(--sp-4); margin:0 0 var(--sp-5); }
.identity-preview>div { min-width:0; }
.identity-preview>.identity-face { width:72px; height:72px; }
.identity-preview strong,.identity-preview small { display:block; }
.identity-preview small { font-size:var(--fs-sm); line-height:var(--lh-base); color:var(--text-secondary); margin-top:var(--sp-1); }
.identity-dialog .identity-presets { display:grid; grid-template-columns:repeat(auto-fit,minmax(44px,1fr)); gap:var(--sp-2); max-height:248px; overflow:auto; padding:var(--sp-1); }
.identity-dialog .identity-presets button { display:block; aspect-ratio:1; padding:2px; border:2px solid transparent; background:none; border-radius:var(--r-full); min-width:44px; min-height:44px; }
.identity-presets img { display:block; width:100%; height:100%; border-radius:var(--r-full); object-fit:contain; }
.identity-dialog .identity-presets [aria-checked=true] { border-color:var(--accent-line); background:var(--state-selected); }
.native-dialog .identity-presets [aria-checked=true]:focus-visible { outline:2px solid var(--accent-line); outline-offset:2px; }
.identity-colors { display:flex; align-items:center; gap:var(--sp-2); flex-wrap:wrap; }
.identity-colors>button { flex-shrink:0; width:44px; height:44px; padding:0; border:2px solid transparent; border-radius:var(--r-full); cursor:pointer; position:relative; }
.identity-colors [aria-pressed=true] { border-color:var(--accent-line); box-shadow:none; }
.identity-colors>button[aria-pressed=true]:focus-visible { outline:2px solid var(--accent-line); outline-offset:2px; }
.identity-colors .identity-custom { display:flex; align-items:center; gap:var(--sp-2); font-size:var(--fs-sm); }
.identity-custom input { width:44px; height:44px; padding:var(--sp-1); border:1px solid var(--border-default); border-radius:var(--r-sm); background:var(--fill-subtle); cursor:pointer; }
.identity-error { font-size:var(--fs-md); color:var(--danger-text); }
.view-author{display:flex;align-items:center;gap:8px;margin-left:auto;font-size:12px}.view-author .identity-face{width:28px;height:28px}.comment-avatar{overflow:hidden}
@media(max-width:540px){.identity-row{gap:8px}.identity-row small{max-width:180px}.view-author{max-width:160px}.view-author>span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}
/* Imported bookmarks stay light until a person opens their page. */
.bookmark-page { height: 100%; box-sizing: border-box; display: flex; flex-direction: column; gap: calc(5 * var(--px, 1px)); padding: calc(8 * var(--px, 1px)); background: var(--surface-raised); color: var(--text-primary); font-size: calc(12 * var(--px, 1px)); }
.bookmark-host { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bookmark-path { color: var(--text-secondary); font-size: calc(11 * var(--px, 1px)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bookmark-actions { display: flex; align-items: center; justify-content: space-between; gap: 6px; margin-top: auto; }
.bookmark-actions .text-btn { display: inline-flex; align-items: center; gap: calc(4 * var(--px, 1px)); padding: calc(4 * var(--px, 1px)) calc(6 * var(--px, 1px)); min-height: calc(24 * var(--px, 1px)); font-size: calc(11 * var(--px, 1px)); white-space: nowrap; }
.bookmark-actions svg { width: calc(12 * var(--px, 1px)); height: calc(12 * var(--px, 1px)); vertical-align: middle; }
.bookmark-card .use-hint { display: none; }
.sub-preview .bookmark-actions { display: none; }

/* Browser boards show the saved desktop appearance. */
.static-web #btn-browse, .static-web [data-act="peek"], .static-web [data-act="expand"],
.static-web .web .use-hint, .static-web .web .live-badge, .static-web .web .live-done { display: none !important; }
.static-web #peek, .static-web #expand { display: none !important; }
.web-still { width: 100%; height: 100%; overflow: hidden; }
.web-still > img { display: block; width: 100%; height: auto; pointer-events: none; }
.web-still-empty { display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 12px; padding: 24px; box-sizing: border-box; text-align: center; color: var(--text-secondary); }
.web-still-empty svg { width: 24px; height: 24px; }
/* On a card the placeholder sits on the page plane (page white under the dark veil), so
   it takes the page's ink, not the app's: in dark mode the app's light text was grey on
   grey and the page's title all but vanished. */
.card.web .web-still-empty { color: color-mix(in oklab, var(--text-page), var(--surface-page) 28%); } /* a step deeper than the chrome's ink: under the dark veil that was ~4.4:1 */
.card.web .web-still-empty strong { color: var(--text-page); }
/* browser-start.css draws the link placeholder with its own secondary ink (the chrome's 38% mix); on a card that
   ink sits under the dark veil too, so it takes the same deeper step. Side Peek and Expand keep theirs. */
.card.web .browser-page { --browser-secondary: color-mix(in oklab, var(--text-page), var(--surface-page) 28%); }

.canvas-room-preview { display:flex; flex-direction:column; justify-content:center; gap:18px; height:100%; box-sizing:border-box; padding:36px; color:var(--text-primary); }
.canvas-room-preview > span { color:var(--text-tertiary); }
.canvas-room-preview > strong { font-size:28px; line-height:1.3; }
.canvas-room-preview > p { margin:0; font-size:18px; line-height:1.6; color:var(--text-secondary); }
