/* =========================================================================
   LinkedDataHub App UI kit — composition styles
   Builds on ../../colors_and_type.css and the core primitives. Keep tokens here
   to a minimum; this file is for layout / component skins.
   ========================================================================= */

/* Core primitives (incl. the .msi icon base) — app pages link app.css directly,
   so the dependency is declared here rather than assumed from styles.css. */
@import url('core.css');

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { background: var(--bg-page); color: var(--fg-1); font-family: var(--font-sans); }

a { color: var(--fg-link); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 2px; }

button { font-family: inherit; cursor: pointer; }

/* Material Symbols (.msi) now lives in ui_kits/core/core.css — the icon
   primitive is core-level. app.css inherits it via styles.css. */

/* ---------- App shell ---------- */
.app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* Chrome geometry, declared once. The rail's top offset is derived from these,
   so changing a height cannot silently misalign it. */
:root {
  --ldh-header-h: 56px;
  --ldh-tabs-h: 40px;
  --ldh-chrome-h: calc(var(--ldh-header-h) + var(--ldh-tabs-h));
  --ldh-rail-w: 264px;
}

/* The rail is a DRAWER: it slides over the content, it does not push it.
   It was briefly changed to push (offsetting .ldh-actionbar/.ldh-main/.ldh-footer
   by --ldh-rail-w) to stop it obscuring blocks. That fixed a symptom by changing
   what the component IS: a drawer legitimately covers content, and earns it by
   being evidently temporary — hence the backdrop below and dismiss-on-click.
   Push also reflowed the whole document on every open, and re-wrapped the text
   the user was reading. */

/* Backdrop. One z below the rail, above everything else, so the covered content
   reads as behind a temporary panel rather than as broken layout — and clicking
   it closes, which is the affordance that makes covering acceptable. */
.ldh-rail-backdrop {
  position: fixed; inset: 0; z-index: 29;
  background: rgba(15, 18, 26, 0.32);
  -webkit-backdrop-filter: blur(1px); backdrop-filter: blur(1px);
  border: 0; padding: 0; cursor: pointer;
  animation: ac-fade-in var(--dur-base) var(--ease-out-soft);
}

/* Top header — dark accent gradient with knowledge-graph dot pattern overlay.
   STICKY, not relative: the rail is position:fixed and offset below the chrome,
   so if the chrome scrolled away the rail would hang in the middle of the page
   with a 96px band of content showing above it. Sticky keeps the offset true at
   any scroll position, which is what makes "the rail belongs to the tab you are
   in" readable rather than just asserted. */
.ldh-header {
  position: sticky; top: 0; z-index: 40;
  height: var(--ldh-header-h);
  padding: 0 var(--sp-6);
  display: grid;
  grid-template-columns: 220px 1fr auto;
  align-items: center;
  gap: var(--sp-6);
  background: var(--grad-deep);
  color: #fff;
  flex-shrink: 0;
  isolation: isolate; /* contain ::before stacking without clipping descendants */
}
.ldh-header::before {
  content: "";
  position: absolute; inset: 0;
  background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.10) 1px, transparent 1.4px);
  background-size: 22px 22px;
  pointer-events: none;
  opacity: 0.55;
  z-index: -1; /* sit under header children so dropdowns can overflow downward */
}
.ldh-header > * { position: relative; }
/* The 220px wordmark track is load-bearing at desktop: ldh.css computes --address-offset by
   subtracting exactly 220px from --wing so the address bar starts on the content column's edge.
   So the track stays 220px above the stop and only the narrow branch reshapes it - which is also
   what "readable, not redesigned" asks for. Below the stop --wing clamps to 0 and the offset
   with it, so the arithmetic is a no-op there and nothing is left dangling.
   220 + 120 of actions + 2 gaps + 2 paddings is 412px of chrome on a 390px screen: the address
   bar computed 26px. Dropping the wordmark to its mark and tightening the gutters gives it ~198px. */
@media (max-width: 768px) {
  .ldh-header { grid-template-columns: auto minmax(0, 1fr) auto; padding: 0 var(--sp-3); gap: var(--sp-3); }
  .ldh-wordmark > span:not(.mark) { display: none; }
}

.ldh-wordmark {
  display: flex; align-items: center; gap: var(--sp-2);
  font-family: var(--font-sans);
  font-weight: var(--fw-semibold);
  font-size: 16px;
  letter-spacing: var(--tracking-snug);
  color: #fff;
  text-decoration: none;
}
.ldh-wordmark .mark {
  width: 24px; height: 24px;
  border-radius: 6px;
  background: linear-gradient(135deg, var(--ldh-blue-400) 0%, var(--ldh-violet-400) 100%);
  position: relative;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.18);
}
.ldh-wordmark .mark::before, .ldh-wordmark .mark::after {
  content: ""; position: absolute; border-radius: 50%; background: rgba(255,255,255,0.9);
}
.ldh-wordmark .mark::before { width: 6px; height: 6px; top: 4px; left: 4px; }
.ldh-wordmark .mark::after  { width: 4px; height: 4px; bottom: 5px; right: 5px; }

.ldh-address {
  display: flex; align-items: center;
  height: 36px;
  min-width: 0;
  width: 100%;
  max-width: 720px;
  padding: 0 12px;
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.14);
  border-radius: var(--r-md);
  color: #fff;
  gap: 8px;
  justify-self: center;
}
.ldh-address:focus-within {
  background: rgba(255,255,255,0.16);
  border-color: rgba(255,255,255,0.32);
  box-shadow: 0 0 0 4px rgba(77, 148, 248, 0.18);
}
.ldh-address .msi { color: rgba(255,255,255,0.72); }
.ldh-address input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: 0;
  outline: 0;
  color: #fff;
  font-family: var(--font-mono);
  letter-spacing: 0.02em;
  font-size: var(--fs-sm);
}
.ldh-address input::placeholder { color: rgba(255,255,255,0.55); font-family: var(--font-mono); letter-spacing: 0.02em; }

.ldh-search {
  display: flex; align-items: center;
  height: 36px;
  background: rgba(255,255,255,0.10);
  border: 1px solid rgba(255,255,255,0.14);
  border-radius: var(--r-md);
  padding: 0 var(--sp-3);
  gap: var(--sp-2);
  color: rgba(255,255,255,0.92);
  transition: background var(--dur-base) var(--ease-out-soft), border-color var(--dur-base) var(--ease-out-soft);
  max-width: 720px;
  width: 100%;
  justify-self: center;
}
.ldh-search:focus-within {
  background: rgba(255,255,255,0.16);
  border-color: rgba(255,255,255,0.32);
  box-shadow: 0 0 0 4px rgba(77, 148, 248, 0.18);
}
.ldh-search .msi { color: rgba(255,255,255,0.72); }
.ldh-search input {
  flex: 1;
  background: transparent;
  border: 0;
  outline: 0;
  color: #fff;
  font-family: var(--font-sans);
  font-size: var(--fs-sm);
}
.ldh-search input::placeholder { color: rgba(255,255,255,0.55); font-family: var(--font-mono); letter-spacing: 0.02em; }
.ldh-search kbd {
  font-family: var(--font-mono);
  font-size: 10px;
  color: rgba(255,255,255,0.55);
  padding: 2px 6px;
  border: 1px solid rgba(255,255,255,0.18);
  border-radius: 4px;
}

.ldh-header-actions { display: flex; align-items: center; gap: var(--sp-2); }
/* The header's icon buttons take the HEADER's ink by inheritance, never a
   hardcoded literal: m3 makes the bar dark (light ink) while base44 makes it
   light (dark ink), and `intent="inverse"` would pin white in both. Hover tints
   with currentColor so it lands correctly on either. This is why the button
   needs no per-skin colour patch at all. */
.ldh-header .ac-iconbtn { color: inherit; }
.ldh-header .ac-iconbtn:hover { background: color-mix(in srgb, currentColor 12%, transparent); color: inherit; }
.ldh-header .ac-iconbtn:active { background: color-mix(in srgb, currentColor 18%, transparent); }

/* Header icon buttons are core <IconButton size="lg">; the
   unread dot is a core <Badge dot>. The old .ldh-icon-btn hardcoded
   rgba(255,255,255,.10) and rgba(10,45,87,.95) — raw values, not tokens. */

.ldh-avatar-wrap { position: relative; display: inline-flex; }
/* Account avatar. A solid accent disc rather than a translucent one: the header
   bar is dark under m3 and light under base44, and --bg-accent / --fg-on-accent
   is the one pair that stays legible on both without a per-skin patch. */
.ldh-avatar {
  width: 32px; height: 32px; flex-shrink: 0; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%;
  background: var(--bg-accent); color: var(--fg-on-accent);
  font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  letter-spacing: 0.02em; cursor: pointer;
  transition: filter var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-fast) var(--ease-out-soft);
}
.ldh-avatar:hover { filter: brightness(1.08); }
.ldh-avatar:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
.ldh-avatar.is-open { box-shadow: 0 0 0 2px var(--surface-0), 0 0 0 4px var(--bg-accent); }

/* Header icon buttons take the bar's own ink instead of a fixed intent: m3
   renders a dark bar and base44 a light one, so a hardcoded inverse would be
   invisible in one of them. .ldh-header sets the ink once; these inherit it and
   tint their hover from currentColor. */
.ldh-header .ac-iconbtn { color: inherit; opacity: 0.82; }
.ldh-header .ac-iconbtn:hover {
  opacity: 1; color: inherit;
  background: color-mix(in srgb, currentColor 14%, transparent);
}
.ldh-header .ac-iconbtn:active { background: color-mix(in srgb, currentColor 22%, transparent); }
.ldh-user-menu {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: min(248px, calc(100vw - 2 * var(--sp-4)));
  background: var(--bg-card);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  box-shadow: 0 16px 40px -12px rgba(15, 23, 42, 0.28), 0 4px 12px -4px rgba(15, 23, 42, 0.18);
  padding: var(--sp-2);
  z-index: 70;
  animation: menu-pop var(--dur-fast) var(--ease-out-soft);
  color: var(--fg-1);
}
@keyframes menu-pop {
  from { opacity: 0; transform: translateY(-4px) scale(0.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.ldh-user-menu .umhead {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2);
}
.ldh-user-menu .umavatar {
  width: 36px; height: 36px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--ldh-blue-400), var(--ldh-violet-400));
  color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.04em;
  flex-shrink: 0;
}
.ldh-user-menu .umid { min-width: 0; }
.ldh-user-menu .umname {
  font-weight: var(--fw-semibold);
  font-size: var(--fs-sm);
  color: var(--fg-1);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ldh-user-menu .umemail {
  font-size: 11px;
  color: var(--fg-2);
  font-family: var(--font-mono);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ldh-user-menu .umdiv {
  height: 1px;
  background: var(--border-default);
  margin: var(--sp-1) 0;
}
.ldh-user-menu .umitem {
  display: flex; align-items: center; gap: var(--sp-2);
  width: 100%;
  padding: var(--sp-2);
  border: 0;
  background: transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
  font-size: var(--fs-sm);
  color: var(--fg-1);
  text-align: left;
  font-family: inherit;
}
.ldh-user-menu .umitem:hover { background: var(--surface-2); }
.ldh-user-menu .umitem.is-danger { color: var(--danger-500); }
.ldh-user-menu .umitem.is-danger:hover { background: rgba(180, 35, 24, 0.08); }
.ldh-user-menu .umitem .msi { color: inherit; }

/* ---------- Tabs row (dataspaces) ---------- */
/* Tab strip — ul > li > a + close. The sticky bar surface lives here (not in a
   consumer stylesheet): surface-1 ground, bottom border, underline-active.
   Flex on the UL, so the LI is the item that carries the active treatment. */
.ldh-tabs { list-style: none; margin: 0; padding: 0 var(--sp-4); }
.ldh-tabs > li {
  display: inline-flex; align-items: center; gap: 2px;
  border-bottom: 2px solid transparent;
  transition: border-color var(--dur-fast) var(--ease-out-soft), background var(--dur-fast) var(--ease-out-soft);
}
.ldh-tabs > li:hover { background: var(--bg-hover); }
/* The underline spans the whole item, close button included, which is why the
   state is on the LI rather than the anchor. */
.ldh-tabs > li.is-active { border-bottom-color: var(--fg-selected); }
.ldh-tabs > li.is-active > .ldh-tab { color: var(--fg-selected); font-weight: var(--fw-medium); }
.ldh-tabs > li > .ldh-tab {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: 0 var(--sp-2); height: 100%;
  color: var(--ink-3); text-decoration: none; white-space: nowrap;
}
.ldh-tabs > li > .ldh-tab:hover { color: var(--fg-1); text-decoration: none; }
.ldh-tabs > li > .ldh-tab:focus-visible { outline: 0; box-shadow: var(--focus-ring); border-radius: var(--r-xs); }

/* Legacy nav>button anatomy — kept one cycle while the bundle recompiles. */
.ldh-tabs {
  display: flex;
  align-items: stretch;
  gap: 2px;
  padding: 0 var(--sp-6);
  background: var(--surface-1);
  border-bottom: 1px solid var(--border-default);
  height: var(--ldh-tabs-h);
  box-sizing: border-box;
  /* Tabs are nowrap by nature - a dataspace tab that wrapped mid-label would be worse than one
     scrolled past - so the strip needs a scrollport, which it had neither of. With no flex-wrap and
     no overflow, the SECOND open dataspace already pushed the page wider than the viewport. The
     kit's own .ac-tablist (core.css) has solved this the same way since before this strip existed;
     this adopts it rather than inventing a second answer. scrollbar-width: none matches it too -
     the strip is chrome, and a scrollbar inside 40px of chrome is louder than the overflow. */
  overflow-x: auto;
  scrollbar-width: none;
}
.ldh-tabs::-webkit-scrollbar { display: none; }
/* Sticky goes on the <nav> WRAPPER, not on the <ul>. A sticky element can only
   travel within its containing block, and the nav is exactly the ul's height —
   so sticking the ul pinned it to a box it already filled and it scrolled away
   with the page. The nav is the element in the document flow, so it is the one
   that can stick.
   One z below the header; both must out-rank the rail (z-index 30), or the rail
   slides over the chrome it hangs from. */
nav:has(> .ldh-tabs) {
  position: sticky; top: var(--ldh-header-h); z-index: 39;
  background: var(--surface-1);
}
.ldh-tab {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  padding: 0 var(--sp-4);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--fg-muted);
  border: 0;
  background: transparent;
  border-bottom: 2px solid transparent;
  transition: color var(--dur-fast) var(--ease-out-soft), border-color var(--dur-fast) var(--ease-out-soft);
}
.ldh-tab:hover { color: var(--fg-1); }
/* Active marker uses the semantic --fg-selected (not a raw ramp step), so it
   matches core's .ac-tab.is-on and follows any skin without a per-skin patch. */
.ldh-tab.is-active { color: var(--fg-selected); border-bottom-color: var(--fg-selected); }
/* Scoped to the STRIP, not to `.ldh-tab`: §17b moved the close button out of
   the anchor to be its sibling in the `li` (a button inside an anchor is
   invalid), so `.ldh-tab .tab-close` stopped matching and it fell back to UA
   button styling — a bordered white box, oversized, sitting outside the tab.
   `.ldh-tabs .tab-close` matches wherever it sits inside the strip, so it also
   covers the legacy nested markup while a stale bundle is still serving it.
   It is a real <button> now, so it needs the reset an inert <span> did not. */
.ldh-tabs .tab-close {
  width: 18px; height: 18px; flex-shrink: 0; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: transparent; cursor: pointer;
  font: inherit; line-height: 1;
  border-radius: var(--r-xs); color: var(--fg-hint);
  transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft);
}
.ldh-tabs .tab-close:hover { background: var(--bg-hover); color: var(--fg-1); }
.ldh-tabs .tab-close:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
.ldh-tabs .tab-close .msi { font-size: 14px; }

/* ---------- Action bar (Add / Breadcrumb / Toolbar) ---------- */
/* Action bar Create dropdown — Container / Item picker. */
.ldh-add-wrap { position: relative; display: inline-flex; }
.ldh-add-wrap .ac-btn.is-open .caret { transform: rotate(180deg); }
.ldh-add-wrap .ac-btn .caret { transition: transform var(--dur-fast) var(--ease-out-soft); }
.ldh-add-menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  min-width: min(280px, calc(100vw - 2 * var(--sp-4)));
  background: var(--bg-card);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  box-shadow: 0 16px 40px -12px rgba(15, 23, 42, 0.28), 0 4px 12px -4px rgba(15, 23, 42, 0.18);
  padding: var(--sp-2);
  z-index: 50;
  animation: menu-pop var(--dur-fast) var(--ease-out-soft);
  color: var(--fg-1);
  /* THE OVERFLOW RULE, as the core Menu states it (../core/overlays.css): a panel that outgrows the
     side it opened on scrolls inside that side instead of running out of it. This menu is the one
     that needs it most - its items are a data set (the ontology's classes), so its height is not
     something the design can look at once and call settled. Flipping alone does not save it: opened
     upward out of the create dock it ran 690px into a 280px gap, and what overran went under the
     sticky header and action bar, which paint over it - the dock is a stacking context, so no
     z-index on the panel can outrank them.
     The ceiling is the core Menu's 340px. Under it, the consumer stamps --ldh-menu-avail on the
     anchor when the menu opens, carrying the room the chosen side actually has (up to the sticky
     chrome's lower edge, not the viewport's), which is the measurement only the running page can
     make. Unstamped - these preview pages - the fallback leaves the flat 340px ceiling in force. */
  max-height: clamp(120px, var(--ldh-menu-avail, 340px), 340px);
  overflow-y: auto;
}
.ldh-add-menu .hd {
  padding: 6px 8px 4px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
.ldh-add-menu .it {
  display: grid;
  grid-template-columns: 32px 1fr;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px;
  border: 0;
  background: transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
  font: inherit;
  text-align: left;
  color: var(--fg-1);
}
.ldh-add-menu .it:hover { background: var(--surface-1); }
.ldh-add-menu .it .ico {
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-sm);
  background: var(--bg-accent-quiet);
  color: var(--ldh-blue-700);
}
.ldh-add-menu .it .body { display: flex; flex-direction: column; min-width: 0; }
.ldh-add-menu .it .body .lbl {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--fg-1);
}
.ldh-add-menu .it .body .sub {
  font-size: var(--fs-xs);
  color: var(--fg-2);
}

.ldh-actionbar {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--sp-6);
  padding: var(--sp-3) var(--sp-6);
  background: var(--surface-0);
  border-bottom: 1px solid var(--border-default);
  flex-shrink: 0;
}
/* Both outer tracks are `auto`, i.e. max-content clusters of nowrap buttons, and a grid track does
   not shrink below max-content - so when the three zones no longer fit one row they OVERLAP rather
   than compress. Measured at 390px: the Create label sat on top of the mode switcher's label and the
   breadcrumb ran under the Actions icon. Letting the bar grow taller does not help; nothing in a
   single grid row has anywhere to go.
   Flex-wrap gives them somewhere. The layout it produces is the one the design system already
   describes for a narrow bar - .ldh-actionbar--twotier, "location line above, controls line below" -
   reached here through the zoned/contextual DOM, which has .ab-mid where twotier has .ab-context.
   order: -1 puts the location line first without reordering the markup, and the controls keep their
   left/right split on the line below.

   Measured on the Northwind dashboard, overlapping pairs inside the bar / breadcrumb width:
     before   390: 9 pairs, breadcrumb 0px   480: 8, 0px   560: 4, 10px   640: 2, 71px   768: 0, 71px
     after    every width: 0 pairs, breadcrumb 71px
   So the breadcrumb was not merely cramped below 480 - it was gone, and the zones were painting over
   each other as far up as 640.

   The stop is the shell's 768 rather than the 640 where overlap happened to stop on THIS document:
   the width at which the zones collide depends on how deep the breadcrumb is, so any exact number is
   right for one page and wrong for the next. 768 errs safe and stays on the scale. It costs ~23px of
   bar height between 640 and 768, where this document had no overlap - paid deliberately. Forcing the
   full-width line was also SHORTER than letting flex wrap naturally (87px vs 97px at 560-768), since
   the controls then share one compact line instead of spilling onto a third. */
@media (max-width: 768px) {
  .ldh-actionbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); padding: var(--sp-2) var(--sp-3); }
  .ldh-actionbar > .ab-mid { flex: 1 0 100%; order: -1; min-width: 0; }
  .ldh-actionbar > .ldh-actions { margin-left: auto; }
}

/* Buttons live in ui_kits/core/core.css as .ac-btn — the ONE button system.
   The app kit's legacy .ldh-btn is gone; markup uses the core class directly. */

/* ---------- Breadcrumb · Drive (Google-Drive-flavored, default) ----------
   Roomy horizontal path. Ancestors are quiet medium-weight links with a hover
   pill; the current resource is large + bold with a dropdown caret. */
.ldh-bc-drive {
  display: flex; align-items: center;
  gap: 2px;
  min-width: 0;
}
.ldh-bc-drive .bc-d-up {
  display: inline-flex; align-items: center;
  padding: 4px 9px;
  border-radius: var(--r-md);
  color: var(--fg-2);
  font-size: var(--fs-base);
  font-weight: var(--fw-medium);
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft);
}
.ldh-bc-drive .bc-d-up:hover {
  background: var(--surface-2);
  color: var(--fg-1);
  text-decoration: none;
}
.ldh-bc-drive .bc-d-sep {
  color: var(--fg-hint);
  font-size: 18px;
  opacity: 0.7;
  flex-shrink: 0;
}
.ldh-bc-drive .bc-d-here {
  display: inline-flex; align-items: center;
  padding: 4px 9px;
  border-radius: var(--r-md);
  font: inherit;
  color: var(--fg-1);
  min-width: 0;
}
.ldh-bc-drive .bc-d-name {
  font-size: var(--fs-base);
  font-weight: var(--fw-medium);
  color: var(--fg-1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

/* ---------- Breadcrumb · Stack (modern hero) ----------
   Two stacked lines in the middle of the actionbar:
     · Parent path — tiny uppercase mono "you came from here" trail.
     · Current — large bold name + icon chip + optional class badge.
   Reads as a "you are here" hero rather than a list of equal links. */
.ldh-bc-stack {
  display: flex; flex-direction: column;
  justify-content: center;
  gap: 2px;
  min-width: 0;
  line-height: 1.2;
}
.ldh-bc-stack .bc-path {
  display: flex; align-items: center; gap: 4px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-muted);
  min-width: 0;
}
.ldh-bc-stack .bc-up {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 4px;
  border-radius: 4px;
  color: inherit;
}
.ldh-bc-stack .bc-up:hover {
  background: var(--surface-2);
  color: var(--fg-2);
  text-decoration: none;
}
.ldh-bc-stack .bc-up .msi { font-size: 13px; opacity: 0.7; }
.ldh-bc-stack .bc-sep {
  color: var(--fg-hint);
  opacity: 0.55;
  font-size: 11px;
}
.ldh-bc-stack .bc-here {
  display: inline-flex; align-items: center; gap: 8px;
  min-width: 0;
}
.ldh-bc-stack .bc-here-ico {
  width: 24px; height: 24px;
  border-radius: 6px;
  background: var(--bg-accent-quiet);
  color: var(--ldh-blue-700);
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.ldh-bc-stack .bc-here-ico .msi { font-size: 16px; }
.ldh-bc-stack .bc-here-name {
  font-size: 15px;
  font-weight: var(--fw-semibold);
  color: var(--fg-1);
  letter-spacing: -0.005em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.ldh-bc-stack .bc-here-badge {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.02em;
  color: var(--ldh-blue-700);
  background: var(--bg-accent-quiet);
  border: 1px solid var(--ldh-blue-100);
  padding: 1px 6px;
  border-radius: var(--r-pill);
  flex-shrink: 0;
}

/* ---------- Breadcrumb · Pills ---------- */
.ldh-bc-pills {
  display: flex; align-items: center; gap: 4px;
  /* NOWRAP on purpose: see .ab-mid above. A wrapping breadcrumb stops reading
     as a path. Overflow is absorbed by truncating pill labels instead. */
  flex-wrap: nowrap;
  min-width: 0;
  /* Truncation runs out before a phone does: the current pill is flex-shrink: 0 and every
     separator is too, so past a few levels the trail pushes the action bar wider than the screen.
     A scrollport, not a wrap - the nowrap above is a reading decision, and the trail stays a trail
     whether or not all of it is on screen. Same answer as the dataspace tab strip, for the same
     reason. */
  overflow-x: auto;
  scrollbar-width: none;
}
.ldh-bc-pills::-webkit-scrollbar { display: none; }
.ldh-bc-pills .bc-pill {
  display: inline-flex; align-items: center; gap: 6px;
  min-width: 0;
  padding: 4px 10px;
  height: 26px;
  border-radius: var(--r-pill);
  background: var(--surface-1);
  border: 1px solid transparent;
  color: var(--fg-2);
  font-size: var(--fs-sm);
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft);
}
.ldh-bc-pills .bc-pill:hover { background: var(--bg-hover); color: var(--fg-1); text-decoration: none; }
.ldh-bc-pills .bc-pill.is-current {
  background: var(--bg-accent-quiet);
  border-color: var(--ldh-blue-100);
  color: var(--ldh-blue-700);
  font-weight: var(--fw-medium);
}
.ldh-bc-pills .bc-pill .msi { font-size: 14px; opacity: 0.85; flex-shrink: 0; }
/* The label truncates; the icon and the current pill do not. Keeping the
   current step whole matters most — it is where you are. */
.ldh-bc-pills .bc-pill > span:not(.msi) { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.ldh-bc-pills .bc-pill.is-current { flex-shrink: 0; }
.ldh-bc-pills .bc-sep { flex-shrink: 0; }
.ldh-bc-pills .bc-sep {
  color: var(--fg-hint);
  font-size: 14px;
  opacity: 0.7;
}

/* ---------- Breadcrumb · Compact (iOS-style back + current) ---------- */
.ldh-bc-compact {
  display: inline-flex; align-items: center; gap: 8px;
  min-width: 0;
}
.ldh-bc-compact .bc-back {
  display: inline-flex; align-items: center; gap: 2px;
  padding: 4px 8px 4px 4px;
  border-radius: var(--r-sm);
  color: var(--fg-link);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  white-space: nowrap;
}
.ldh-bc-compact .bc-back:hover { background: var(--surface-2); text-decoration: none; }
.ldh-bc-compact .bc-back .msi { font-size: 14px; }
.ldh-bc-compact .bc-divider {
  width: 1px; height: 20px;
  background: var(--border-default);
}
.ldh-bc-compact .bc-here-compact {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--fs-sm);
  font-weight: var(--fw-semibold);
  color: var(--fg-1);
  white-space: nowrap;
}
.ldh-bc-compact .bc-here-compact .msi {
  color: var(--ldh-blue-700);
}

/* The Stack style is taller than the rest. Adjust actionbar vertical padding
   to keep the row visually balanced regardless of style. */
.ldh-actionbar--bc-stack { padding-top: var(--sp-2); padding-bottom: var(--sp-2); }
.ldh-actionbar--bc-drive { padding-top: var(--sp-2); padding-bottom: var(--sp-2); }

/* ─────────────────────────────────────────────────────────────────────────
   Tweak-driven overrides — toggled by data-attributes on <html>.
   Set by index.html based on the Tweaks panel state.
   ───────────────────────────────────────────────────────────────────────── */


/* Density — compact tightens row padding across the property editor. */
html[data-tweak-density="compact"] .ldh-prop-row > .value,
html[data-tweak-density="compact"] .ldh-prop-row > .row-actions {
  padding-top: 4px;
  padding-bottom: 4px;
}
html[data-tweak-density="compact"] .ldh-prop-group > .label {
  padding-top: 4px;
  padding-bottom: 4px;
}
html[data-tweak-density="compact"] .ldh-actionbar {
  padding-top: 4px;
  padding-bottom: 4px;
}
html[data-tweak-density="compact"] .ldh-block-body { padding: var(--sp-3) 0; }

/* Header style — subtle = solid dark navy, no gradient. Flat = light card. */
html[data-tweak-header="subtle"] .ldh-header {
  background: var(--ldh-blue-900, #0a1230);
}
html[data-tweak-header="subtle"] .ldh-header::before { opacity: 0; }
html[data-tweak-header="flat"] .ldh-header {
  background: var(--bg-card);
  color: var(--fg-1);
  border-bottom: 1px solid var(--border-default);
}
html[data-tweak-header="flat"] .ldh-header::before { opacity: 0; }
html[data-tweak-header="flat"] .ldh-wordmark { color: var(--fg-1); }
html[data-tweak-header="flat"] .ldh-wordmark .mark {
  background: linear-gradient(135deg, var(--ldh-blue-400), var(--ldh-violet-400));
}
html[data-tweak-header="flat"] .ldh-address {
  background: var(--surface-1);
  border-color: var(--border-default);
  color: var(--fg-1);
}
html[data-tweak-header="flat"] .ldh-address input { color: var(--fg-1); }
html[data-tweak-header="flat"] .ldh-address input::placeholder { color: var(--fg-muted); }
html[data-tweak-header="flat"] .ldh-address .msi { color: var(--fg-muted); }
html[data-tweak-header="flat"] .ldh-avatar { border: 2px solid var(--border-default); }

/* Block edges — sharp = no radius, borderless = no border + no radius. */
html[data-tweak-block-edges="sharp"] .ldh-block,
html[data-tweak-block-edges="sharp"] .ldh-view {
  border-radius: 0;
}
html[data-tweak-block-edges="borderless"] .ldh-block,
html[data-tweak-block-edges="borderless"] .ldh-view {
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
html[data-tweak-block-edges="borderless"] .ldh-block-head {
  border-bottom: 1px solid var(--border-default);
}

/* Flow — the document treatment: blocks dissolve their card frame (no fill,
   border, radius or shadow); the data they contain (grid cards, chart plots,
   statement rows) keeps its own surfaces. Consecutive blocks are separated by
   a hairline + even vertical rhythm, so prose and structured views interleave
   as one continuous reading surface rather than a stack of widgets. */
html[data-tweak-block-edges="flow"] .ldh-block,
html[data-tweak-block-edges="flow"] .ldh-view {
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
html[data-tweak-block-edges="flow"] .ldh-block:hover {
  box-shadow: none;
  transform: none;
}
html[data-tweak-block-edges="flow"] .ldh-content { gap: 0; }
html[data-tweak-block-edges="flow"] .ldh-block-row { padding: var(--sp-6) 0; }
html[data-tweak-block-edges="flow"] .ldh-block-row + .ldh-block-row {
  border-top: 1px solid var(--border-default);
}
/* …but data-visualisation bodies still want a white ground — a chart plotted
   straight onto the tinted floor reads as unanchored. Plate the chart body
   (its own controls/canvas padding supplies the inset) so the plot sits on a
   clean surface while prose blocks stay transparent. */
html[data-tweak-block-edges="flow"] .ldh-block-body:has(> .chart-canvas),
html[data-tweak-block-edges="flow"] .ldh-block-body:has(> .chart-controls) {
  background: var(--surface-0);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  padding: 0;
  overflow: hidden;
}
/* Resource blocks are structured data surfaces too: the statement list — and
   especially a media-header block, whose full-bleed image must keep its corners
   clipped — stay on a white card in flow mode (like charts), so the description
   doesn't float transparent over the tinted floor. */
html[data-tweak-block-edges="flow"] .ldh-block:has(.ldh-edit-form) {
  background: var(--bg-card);
  border: 1px solid var(--border-default);
  border-radius: var(--r-lg);
  overflow: hidden;
}

/* Action toolbar (right cluster) */
.ldh-toolbar {
  display: flex; align-items: center; gap: var(--sp-1);
  color: var(--fg-muted);
}
.ldh-toolbar .ts {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--fg-hint);
  margin-right: var(--sp-3);
  white-space: nowrap;
}
/* =========================================================================
   Icon buttons: core <IconButton> only.
   `.tb` and `.rb` are gone. They were the app kit's own icon-button classes,
   doing the same job as the core primitive with their own sizes, hover and
   focus rules — the last duplicated primitive in the system. Sizes map
   xs 28 / sm 32 / md 34 / lg 36, intents neutral | destructive | accent,
   toggles use `pressed`, and a tally uses `count`.
   ========================================================================= */
.ldh-toolbar .divider {
  width: 1px; height: 20px; background: var(--border-default); margin: 0 var(--sp-2);
}

/* ─── Redesigned action cluster (declutter): mode · contextual primary · ⋯ ── */
.ldh-actions { display: flex; align-items: center; gap: var(--sp-1); color: var(--fg-muted); }
.ldh-actions-div { width: 1px; height: 20px; background: var(--border-default); margin: 0 var(--sp-2); }
.ldh-actions .ac-btn .msi { font-size: 18px; }

/* Document dateTime as quiet metadata — never a control */
.ldh-ab-ts {
  display: inline-flex; align-items: center; gap: 4px;
  font-family: var(--font-mono); font-size: var(--fs-xs);
  color: var(--fg-hint); white-space: nowrap; flex-shrink: 0;
}
.ldh-ab-ts .msi { font-size: 14px; opacity: 0.7; }

/* Overflow kebab */
.ldh-of-wrap { position: relative; display: inline-flex; }
/* The kebab is <IconButton size="lg">; only its open state is scoped here. */
.ldh-of-wrap.is-open .ac-iconbtn { background: var(--bg-hover); color: var(--fg-1); }
.ldh-of-menu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 50;
  min-width: min(184px, calc(100vw - 2 * var(--sp-4))); padding: 6px;
  background: var(--surface-0); border: 1px solid var(--border-default);
  border-radius: var(--r-md); box-shadow: var(--shadow-lg);
  display: flex; flex-direction: column; gap: 2px;
  color: var(--fg-1);
  /* the overflow rule, as on .ldh-add-menu above - this panel holds a document's actions, which a
     package can add to, so its height is not fixed either */
  max-height: clamp(120px, var(--ldh-menu-avail, 340px), 340px);
  overflow-y: auto;
}
.ldh-of-menu .it {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 8px 10px; border: 0; background: transparent; cursor: pointer;
  border-radius: var(--r-sm); font: inherit; font-size: var(--fs-sm);
  color: var(--fg-2); text-align: left;
}
.ldh-of-menu .it:hover { background: var(--bg-hover); color: var(--fg-1); }
.ldh-of-menu .it.is-danger { color: var(--danger-500); }
.ldh-of-menu .it.is-danger:hover { background: var(--danger-50); }
.ldh-of-menu .it .msi { font-size: 18px; }
.ldh-of-menu .it-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.ldh-of-menu .it-hint { font-size: var(--fs-xs); color: var(--ink-4); }
.ldh-of-menu .it:hover .it-hint { color: var(--fg-hint); }
.ldh-of-menu .it.is-danger .it-hint { color: var(--ink-4); }
.ldh-of-div { height: 1px; background: var(--border-default); margin: 4px 2px; }

/* Mid zone (breadcrumb + timestamp) for zoned/contextual single-row layouts.
   The timestamp pushes to the right edge of the zone (just before the actions),
   so the "last modified" stamp reads as right-aligned metadata. */
.ldh-actionbar--zoned .ab-mid,
.ldh-actionbar--contextual .ab-mid {
  display: flex; align-items: center; gap: var(--sp-4); min-width: 0;
  /* The mid zone holds two different KINDS of thing: the breadcrumb, which is
     navigation and must stay legible, and the timestamp, which is metadata.
     When they don't both fit, the METADATA yields — it drops to a second line.
     This was nowrap, so the only thing that could give was the breadcrumb's own
     wrap, which stacked the pills one per row and doubled the bar's height. */
  flex-wrap: wrap;
  row-gap: 2px;
}
.ldh-actionbar--zoned .ab-mid .ldh-ab-ts,
.ldh-actionbar--contextual .ab-mid .ldh-ab-ts { margin-left: auto; padding-left: var(--sp-4); }

/* TWO-TIER — location line above, controls line below */
.ldh-actionbar--twotier {
  display: flex; flex-direction: column; align-items: stretch; gap: var(--sp-2);
}
.ldh-actionbar--twotier .ab-context {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4); min-width: 0;
}
.ldh-actionbar--twotier .ab-controls { display: flex; align-items: center; gap: var(--sp-4); }
.ldh-actionbar--twotier .ab-spacer { flex: 1; }

/* CONTEXTUAL — minimal at rest; secondary actions (Edit + ⋯) reveal on hover/
   focus of the bar. Mode switcher stays visible as the one persistent control. */
.ldh-actionbar--contextual .ldh-actions .ac-btn,
.ldh-actionbar--contextual .ldh-actions .ldh-of-wrap,
.ldh-actionbar--contextual .ldh-actions .ldh-actions-div {
  opacity: 0; transition: opacity var(--dur-base) var(--ease-out-soft);
}
.ldh-actionbar--contextual:hover .ldh-actions .ac-btn,
.ldh-actionbar--contextual:focus-within .ldh-actions .ac-btn,
.ldh-actionbar--contextual:hover .ldh-actions .ldh-of-wrap,
.ldh-actionbar--contextual:focus-within .ldh-actions .ldh-of-wrap,
.ldh-actionbar--contextual:hover .ldh-actions .ldh-actions-div,
.ldh-actionbar--contextual:focus-within .ldh-actions .ldh-actions-div { opacity: 1; }

/* Mode switcher — segmented dropdown */
.ldh-mode {
  position: relative;
  display: inline-flex; align-items: center; gap: var(--sp-2);
  height: 36px;
  padding: 0 var(--sp-3);
  background: var(--surface-2);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  color: var(--fg-2);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  margin-right: var(--sp-2);
}
.ldh-mode .label-row { display: inline-flex; align-items: center; gap: var(--sp-2); }
.ldh-mode .label { letter-spacing: 0.02em; }
/* Compact variant — the same dropdown inside a view's own toolbar, where it
   sits beside the facet pills rather than in the page-level action bar. Trigger
   only: the popover keeps its full size, since the mode descriptions are the
   reason it reads better than five unlabelled icons. */
.ldh-mode.sz-sm {
  height: 28px; padding: 0 var(--sp-2); gap: 5px;
  margin-right: 0;
  font-size: var(--fs-xs);
}
.ldh-mode.sz-sm .label-row { gap: 5px; }
.ldh-mode.sz-sm .msi { font-size: 16px; }
.ldh-mode .modes-pop {
  position: absolute; top: calc(100% + 6px); right: 0;
  min-width: min(220px, calc(100vw - 2 * var(--sp-4)));
  background: var(--surface-0);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  padding: var(--sp-1);
  display: none;
  z-index: 50;
  color: var(--fg-1);
  /* the overflow rule, as on .ldh-add-menu above - a short list today, but it opens from the action
     bar, where a short viewport leaves little room below it */
  max-height: clamp(120px, var(--ldh-menu-avail, 340px), 340px);
  overflow-y: auto;
}
.ldh-mode.is-open .modes-pop { display: block; }
.ldh-mode .mi {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-sm); color: var(--fg-2);
  border-radius: var(--r-sm);
  width: 100%;
  background: transparent; border: 0;
  cursor: pointer;
  text-align: left;
}
.ldh-mode .mi:hover { background: var(--bg-hover); color: var(--fg-1); }
.ldh-mode .mi.is-active { background: var(--ldh-blue-50); color: var(--ldh-blue-700); font-weight: var(--fw-medium); }
.ldh-mode .mi .msi { color: var(--fg-muted); }
.ldh-mode .mi.is-active .msi { color: var(--ldh-blue-600); }
.ldh-mode .mi .desc { display: block; font-size: 11px; color: var(--fg-hint); font-weight: 400; margin-top: 1px; }
.ldh-mode .mi .label-col { flex: 1; }

/* ---------- Main layout ---------- */
.ldh-main {
  flex: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-5);
  padding: var(--sp-5) var(--sp-6);
  max-width: var(--container-xl);
  width: 100%;
  margin: 0 auto;
  align-items: start;
}
.ldh-main.is-flush { padding: var(--sp-3) var(--sp-3); max-width: none; }
.ldh-content {
  display: flex; flex-direction: column; gap: var(--sp-6);
  min-width: 0;
}

/* Per-view-block row: the block-scoped drawer (backlinks/related/parallax)
   slides in from the right edge of the block and overlays its right portion.
   The drawer is scoped to the block's height — no global push, no body scroll
   hijack. The row stays display:block so its content keeps full width when
   the drawer is closed AND when it's open. */
.ldh-block-row { position: relative; }
.ldh-block-row > .row-main { min-width: 0; }

/* Backlinks / related panel */
.ldh-panel {
  background: var(--bg-card);
  border: 1px solid var(--border-default);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.ldh-panel .ph {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-3) var(--sp-4);
  background: var(--surface-1);
  border-bottom: 1px solid var(--border-default);
}
.ldh-panel .ph .title {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--fg-muted);
}
.ldh-panel.is-collapsed .pb { display: none; }
.ldh-panel.is-collapsed .ph { border-bottom: 0; }
.ldh-panel .pb { padding: var(--sp-3) var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-2); }
.ldh-panel .pb a { display: block; font-size: var(--fs-sm); padding: 4px 0; color: var(--fg-2); }
.ldh-panel .pb a:hover { color: var(--fg-link); }
.ldh-panel .pb .empty { font-size: var(--fs-sm); color: var(--fg-hint); padding: 4px 0; }

/* ---------- Footer ---------- */
.ldh-footer {
  margin-top: var(--sp-12);
  padding: var(--sp-10) var(--sp-6) var(--sp-8);
  background: var(--surface-1);
  border-top: 1px solid var(--border-default);
}
.ldh-footer .cols {
  max-width: var(--container-xl);
  margin: 0 auto;
  display: grid;
  /* auto-fit, not a fixed five: the columns are independent link lists with nothing to align
     across them, so they can reflow into as many rows as the width allows. The fixed
     `1.4fr repeat(4, 1fr)` never collapsed, and at 390px it alone carried the page's right edge
     out to 616px - 226px of it unreachable, since html{overflow-x:clip} leaves no way to scroll
     there. No breakpoint needed: this is better at every width, not just narrow ones. */
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--sp-8);
}
.ldh-footer .col .ftitle {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--fg-muted);
  margin: 0 0 var(--sp-3);
}
/* The link COLUMNS stack their anchors as blocks. The brand column holds one
   anchor too, and it is not a footer link — it is the wordmark, whose own rule
   lays it out as a flex lockup. This selector outranks that rule (0,2,1 against
   0,1,0), so it was flattening the lockup to display: block; the mark inside it
   became an inline box, which cannot take a width, and collapsed from 24px to
   zero — leaving its gradient as a sliver painted over the first letter. It was
   taking the wordmark's type down to the footer-link size with it. */
.ldh-footer .col a:not(.ldh-wordmark) { display: block; padding: 4px 0; font-size: var(--fs-sm); color: var(--fg-2); }
.ldh-footer .col a:not(.ldh-wordmark):hover { color: var(--fg-link); }
.ldh-footer .brand-col p {
  font-size: var(--fs-sm); color: var(--fg-muted); line-height: var(--lh-base); max-width: 280px; margin: var(--sp-3) 0 0;
}
.ldh-footer .legal {
  max-width: var(--container-xl);
  margin: var(--sp-8) auto 0;
  padding-top: var(--sp-4);
  border-top: 1px solid var(--border-default);
  display: flex; justify-content: space-between;
  font-size: var(--fs-xs);
  color: var(--fg-hint);
  font-family: var(--font-mono);
}

/* ---------- Generic Block (view block wrapper) ---------- */
.ldh-block {
  background: var(--bg-card);
  border: 1px solid var(--border-default);
  border-radius: var(--r-lg);
  /* The card ANCHORS its corner affordances (.ldh-block-corner, the rail) and
     the block-scoped drawer, and must not clip dropdown menus rendered inline
     inside it (.modes-pop, .ldh-add-menu). `overflow: hidden` breaks both —
     it has been re-introduced twice, so: the slot bar fits the card's corners
     with `border-radius: inherit`, which is what clipping was reached for. */
  position: relative;
  overflow: visible;
  /* The block is the query container for everything it holds. A block is as wide as wherever it
     was put - the document column, a nested block, the 300px content aside, a modal body - and its
     contents have to lay out for THAT, not for the viewport. Measured before this existed: the
     statement grid inside a block in the aside got a 36px value column at a 1440px desktop, worse
     than the same grid on a 390px phone. Safe alongside the anchoring above: container-type does
     not reroute the containing block of the absolutely positioned menus (.modes-pop,
     .ldh-add-menu) or of any fixed descendant - verified in the browser, not assumed. */
  container-type: inline-size;
  transition: box-shadow var(--dur-base) var(--ease-out-soft), border-color var(--dur-base) var(--ease-out-soft);
}
.ldh-block:hover { box-shadow: var(--shadow-sm); }
.ldh-block.is-quiet { border: 0; background: transparent; }
.ldh-block.is-quiet:hover { box-shadow: none; }
.ldh-block-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  padding: var(--sp-5) var(--sp-6) var(--sp-3);
  gap: var(--sp-4);
}
.ldh-block-head .ttl { font-size: var(--fs-xl); font-weight: var(--fw-semibold); color: var(--fg-1); margin: 0; line-height: var(--lh-snug); letter-spacing: var(--tracking-snug); }
/* Hero text block — the lead resource's title reads at page-title scale, not
   block-heading scale, so the top of a document anchors the page. */
.ldh-block-head .ttl--hero { font-size: var(--fs-3xl); font-weight: var(--fw-bold); line-height: var(--lh-tight); letter-spacing: var(--tracking-tight); }
.ldh-block-head .sub { display: block; font-size: var(--fs-sm); color: var(--fg-muted); margin-top: 4px; font-weight: 400; }
/* align-items, because the cluster is not icon-only: a block that carries an
   extra labelled action (a view's Create) sets a taller line than the icon
   buttons beside it, and stretched icon buttons would lose their square box. */
.ldh-block-head .actions { display: flex; align-items: center; gap: var(--sp-1); }
/* Actions are core <IconButton>; box, hover and intents come from it. */

/* Links button — opens the block-scoped drawer. It is
   <IconButton intent="accent" count={n}>, so the shape and states come from the
   primitive; only the hero placement is scoped. */
.hero-actions {
  position: absolute;
  top: var(--sp-4);
  right: var(--sp-4);
  display: flex;
  gap: var(--sp-1);
  z-index: 2;
}
/* The hero's links button is core <IconButton intent="accent" count>, and needs
   a translucent ground because it sits on the hero's gradient rather than on a
   card. Scoped to .ac-iconbtn — this rule still said `.tb-links`, a class
   retired when the icon buttons were unified, so it had been dead. */
.hero-actions .ac-iconbtn {
  /* A frosted plate of the CARD surface rather than of white: the hero wash it
     floats over is dark in the dark scheme, where a white plate reads as a hole
     punched in it. Mixing toward transparent keeps the frosting. */
  background: color-mix(in srgb, var(--surface-0) 72%, transparent);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.hero-actions .ac-iconbtn:hover { background: color-mix(in srgb, var(--surface-0) 90%, transparent); }

/* Block drawer — fixed to the viewport's right edge so it sits FLUSH against
   the page edge regardless of where the block lives inside the centered
   .ldh-main container. Top/height are set inline by the React component to
   match the anchor block's bounding rect, so the drawer stays block-scoped
   vertically. */
.ldh-drawer {
  position: fixed;
  right: 0;
  width: min(360px, 92vw);
  color: var(--fg-1);
  background: var(--surface-0);
  border: 0;
  border-left: 1px solid var(--border-default);
  border-radius: 0;
  box-shadow: -8px 0 24px -22px rgba(15, 23, 42, 0.16);
  display: flex; flex-direction: column;
  z-index: 25;
  animation: drawer-slide var(--dur-base) var(--ease-out-soft);
  overflow: hidden;
}
@keyframes drawer-slide {
  from { transform: translateX(100%); opacity: 0; }
  to   { transform: translateX(0);    opacity: 1; }
}

/* Edge-hover sensor — invisible hot-zone flush to the viewport's right edge,
   vertically scoped to its block (top/height set inline). Reaching it slides
   the drawer out, no click needed. A faint handle peeks in so the gesture is
   discoverable. */
.ldh-edge-sensor {
  position: fixed;
  right: 0;
  width: 20px;
  z-index: 24;
  display: flex; align-items: center; justify-content: flex-end;
  cursor: pointer;
}
.ldh-edge-handle {
  display: flex; align-items: center; justify-content: center;
  width: 17px; height: 54px;
  border-radius: var(--r-pill) 0 0 var(--r-pill);
  background: var(--surface-0);
  border: 1px solid var(--border-default);
  border-right: 0;
  color: var(--fg-hint);
  box-shadow: -8px 0 18px -14px rgba(15, 23, 42, 0.28);
  opacity: 0.5;
  transform: translateX(7px);
  transition: opacity var(--dur-base) var(--ease-out-soft),
              transform var(--dur-base) var(--ease-out-soft),
              color var(--dur-fast) var(--ease-out-soft),
              border-color var(--dur-fast) var(--ease-out-soft);
}
.ldh-edge-handle .msi { font-size: 18px; }
.ldh-edge-sensor:hover .ldh-edge-handle {
  opacity: 1;
  transform: translateX(0);
  /* --accent is set only at RUNTIME by index.html's accent tweak, so on every
     other surface (preview cards, templates, screen-*.html) it is undefined and
     the hover colour silently fell back to inherited body ink. The tweak stays
     as an override; --fg-link is the static default because it is the system's
     interactive colour and remaps per skin (base44 → lime). */
  color: var(--accent, var(--fg-link));
  border-color: var(--accent, var(--fg-link));
}
.ldh-drawer .dh {
  display: flex; align-items: flex-start; justify-content: space-between;
  padding: var(--sp-5) var(--sp-5) var(--sp-4);
  border-bottom: 1px solid var(--border-default);
}
.ldh-drawer .dh .eyebrow {
  font-family: var(--font-mono); font-size: 10px;
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--fg-muted);
}
.ldh-drawer .dh h3 {
  margin: 4px 0 0;
  font-size: var(--fs-lg);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-snug);
  color: var(--fg-1);
}
/* Drawer close is core <IconButton size="sm"> — no bespoke rule needed. */

.ldh-drawer .db {
  flex: 1;
  overflow: auto;
  padding: var(--sp-3) var(--sp-3) var(--sp-5);
  display: flex; flex-direction: column; gap: var(--sp-4);
}
.ldh-drawer .dgroup { display: flex; flex-direction: column; gap: 2px; }
.ldh-drawer .dh2 {
  display: flex; align-items: baseline; justify-content: space-between;
  padding: var(--sp-2) var(--sp-2) 4px;
  font-family: var(--font-mono); font-size: 10px;
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--fg-muted);
}
.ldh-drawer .dh2 em {
  font-style: normal;
  font-family: var(--font-mono); font-size: 10px;
  color: var(--fg-hint);
  background: var(--surface-2);
  padding: 1px 6px; border-radius: var(--r-pill);
  letter-spacing: 0;
}
.ldh-drawer .drow {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px;
  border-radius: var(--r-sm);
  text-decoration: none;
  color: var(--fg-2);
  font-size: var(--fs-sm);
  transition: background var(--dur-fast) var(--ease-out-soft);
}
.ldh-drawer .drow:hover { background: var(--bg-hover); color: var(--fg-1); text-decoration: none; }
.ldh-drawer .drow .msi { color: var(--fg-hint); flex-shrink: 0; }
.ldh-drawer .drow .lbl { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ldh-drawer .drow .tag {
  font-family: var(--font-mono); font-size: 9.5px; color: var(--fg-hint);
  background: var(--surface-2); padding: 1px 6px; border-radius: var(--r-pill);
}
.ldh-drawer .drow.is-current {
  background: var(--bg-accent-quiet);
  color: var(--ldh-blue-700);
  font-weight: var(--fw-medium);
}
.ldh-drawer .drow.is-current .msi { color: var(--ldh-blue-600); }
.ldh-drawer .drow.is-current .tag { background: var(--surface-0); color: var(--ldh-blue-600); }

/* Parallax pivot row — a predicate with a direction button each way. Not a
   .drow: a .drow IS the link, whereas here the row is a label and the two
   buttons are the navigation, so the row itself must not be interactive. */
.dpivot {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 4px 4px 4px 8px;
  border-radius: var(--r-sm);
  font-size: var(--fs-sm); color: var(--fg-2);
  transition: background var(--dur-fast) var(--ease-out-soft);
}
.dpivot:hover { background: var(--bg-hover); }
.dpivot-pred { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
.dpivot-pred .msi { color: var(--fg-hint); flex-shrink: 0; }
.dpivot-pred .lbl { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The two directions read as one control, so they sit in a shared track. */
.dpivot-dirs {
  display: flex; align-items: center; gap: 2px; flex-shrink: 0;
  padding: 2px;
  background: var(--surface-2);
  border-radius: var(--r-sm);
}
.dpivot-dirs .ac-iconbtn { color: var(--fg-muted); }
.dpivot-dirs .ac-iconbtn:hover { background: var(--surface-0); color: var(--ldh-blue-700); }
/* A direction with no matches stays visible but inert — its absence would
   wrongly imply the predicate does not exist that way round. */
.dpivot-dirs .ac-iconbtn:disabled { opacity: 0.32; }
.dpivot.is-current { background: var(--bg-accent-quiet); color: var(--ldh-blue-700); font-weight: var(--fw-medium); }
.dpivot.is-current .dpivot-pred .msi { color: var(--ldh-blue-600); }
.dpivot.is-current .dpivot-dirs { background: var(--surface-0); }

.ldh-drawer .df {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--border-default);
  background: var(--surface-1);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--fg-muted);
}
.ldh-block-body { padding: 0 var(--sp-6) var(--sp-6); }
.ldh-block-body.is-flush { padding: 0; }
.ldh-block-foot {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-3) var(--sp-6);
  border-top: 1px solid var(--border-default);
  background: var(--surface-1);
}

/* Section header — for grouping blocks in ContentMode */
.ldh-section {
  display: flex; flex-direction: column; gap: var(--sp-2);
  margin-top: var(--sp-4);
}
.ldh-section .eyebrow { font-family: var(--font-mono); font-size: 11px; letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--ldh-blue-600); }
.ldh-section h2 { margin: 0; font-size: var(--fs-2xl); font-weight: var(--fw-semibold); letter-spacing: var(--tracking-snug); color: var(--fg-1); }
.ldh-section p { margin: 4px 0 0; color: var(--fg-2); max-width: 56ch; }

/* Hero block — uses pastel gradient for the document intro */
.ldh-hero {
  position: relative;
  /* --grad-aurora carries its own dark stops, so the wash follows the scheme and
     the border firms up a step to hold an edge against a dark page. */
  background: var(--grad-aurora);
  border: 1px solid light-dark(var(--border-default), var(--border-strong));
  border-radius: var(--r-lg);
  padding: var(--sp-10) var(--sp-8);
  overflow: hidden;
}
.ldh-hero::before {
  content: ""; position: absolute; inset: 0;
  background-image: radial-gradient(circle at 1px 1px, light-dark(rgba(11,14,20,0.06), rgba(255,255,255,0.08)) 1px, transparent 1.4px);
  background-size: 24px 24px;
  opacity: 0.7;
  pointer-events: none;
}
.ldh-hero > * { position: relative; }

/* Dark theme: pastel hero would strand light text on a light wash — swap to the
   dark accent gradient (governance: pastels/gradients are hero-only anyway). */
.ldh-hero .eyebrow { font-family: var(--font-mono); font-size: 11px; letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--ldh-violet-700); }
.ldh-hero h1 {
  margin: var(--sp-2) 0 var(--sp-3);
  font-size: var(--fs-4xl);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-tight);
  color: var(--fg-1);
  line-height: var(--lh-tight);
}
.ldh-hero h1 em { font-style: italic; font-family: var(--font-display); font-weight: 400; }
.ldh-hero .lede { font-size: var(--fs-md); color: var(--ink-2); max-width: 56ch; line-height: var(--lh-base); margin: 0; }
.ldh-hero .lede + p { margin-top: var(--sp-3); }

/* Chart controls (re-styled <select>s) */
.chart-controls {
  /* Three labelled selects need ~150px each to stay legible. auto-fit lets them take 1, 2 or 3
     columns from the width actually available instead of always splitting it three ways - which in
     a 290px block meant three 70px selects, and in the 300px content aside three 73px ones at a
     full desktop. No breakpoint: the track floor IS the breakpoint, and it travels with the
     component into whatever container it lands in. */
  display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-6);
  background: var(--surface-1);
  border-bottom: 1px dashed var(--border-default);
  border-top: 1px dashed var(--border-default);
}
.chart-controls .field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
/* The controls themselves are core <Label> + <Select>. */
.chart-canvas { padding: var(--sp-6); }
/* ChartMode hero canvas — the chart fills the full block width (fluid SVG),
   so it never gets letterboxed inside a fixed-height box like the small
   dashboard charts do. */
.chart-canvas--hero { padding: var(--sp-5) var(--sp-6) var(--sp-8); }

/* =========================================================================
   RESOURCE BLOCK HEADER — one composite header for every resource.
   Thumbnail (or class icon) · title with its type chips inline · description
   lede · actions. The earlier full-bleed media header is gone: it was a poster
   treatment on a data surface, needed a scrim and its own on-media button
   styles, and gave a resource with an image a structurally different header
   from one without.
   Built from the same parts as every other block head — .ldh-block-head owns
   the padding and the .actions row, and the buttons are core <IconButton>.
   ========================================================================= */
.ldh-res-head { gap: var(--sp-4); align-items: flex-start; }
/* Thumbnail and icon share one footprint, so the title starts at the same x
   whether or not the resource has a depiction. */
.ldh-res-thumb,
.ldh-res-icon {
  width: 72px; height: 72px; flex-shrink: 0;
  border-radius: var(--r-md);
  overflow: hidden;
}
.ldh-res-thumb { display: block; background: var(--surface-2); }
/* image-slot's empty state is sized for a hero-scale target: at 72px its
   caption plus "or browse files" overflows and clips. ::part(empty) is the
   component's own styling hook, so scale the type down there rather than
   fighting the shadow DOM — the icon does the communicating at this size and
   the full prompt lives in the title attribute. */
.ldh-res-thumb::part(empty) {
  font-size: 9px; line-height: 1.15; gap: 3px; padding: 4px;
  overflow: hidden; text-align: center;
}
.ldh-res-icon {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg-accent-quiet); color: var(--ldh-blue-700);
}
.ldh-res-icon .msi { font-size: 32px; }
.ldh-res-text { display: flex; flex-direction: column; gap: 5px; min-width: 0; flex: 1; }
/* Title + type chips on one line; the chips wrap under a long title rather
   than forcing it to truncate. */
.ldh-res-titleline {
  display: flex; align-items: baseline; gap: var(--sp-2);
  flex-wrap: wrap; min-width: 0;
}
.ldh-res-titleline .ttl { margin: 0; }
/* The lede. Capped so a summary stays a summary — the full text is still in
   the statement list when it exceeds the promotion limit. */
.ldh-res-desc {
  margin: 0; max-width: 78ch;
  font-size: var(--fs-base); line-height: var(--lh-base); color: var(--fg-2);
  text-wrap: pretty;
}
/* The block's status line, under the lede. A block whose control chrome is
   collapsed still has to state what the chrome was stating - for a view, how
   many results and what narrowed them - so this carries it: quieter than the
   lede, because it reports rather than describes, with the figures in mono as
   they were in the toolbar's own .count. */
.ldh-view-status {
  margin: 4px 0 0;
  font-size: var(--fs-sm); line-height: var(--lh-snug); color: var(--fg-muted);
}
.ldh-view-status .count { font-family: var(--font-mono); font-size: var(--fs-xs); }
.ldh-view-status .count b { color: var(--fg-1); font-weight: var(--fw-semibold); }
.ldh-view-status .ldh-view-applied b { color: var(--fg-2); font-weight: var(--fw-medium); }
@media (max-width: 560px) {
  .ldh-res-thumb, .ldh-res-icon { width: 56px; height: 56px; }
  .ldh-res-icon .msi { font-size: 26px; }
}

/* Resource list — grid of category cards */
.ldh-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: var(--sp-4);
}
/* Grid cards are core <Card size="sm" tone="…">; the six per-tone rules and the
   .gi/.gt/.gc anatomy moved to .ac-card-ic / -title / -meta. */

/* ---------- Property editor (read + edit on a row level) ----------
   Each row reads as a clean label/value pair by default. Click the value to
   edit it; the visual chrome (input border + bg) only appears while editing.
   No block-wide editing flag; saves are optimistic per-row.

   The READ-mode list rides dl / dt / dd — a resource description is an
   association list, and the product carries RDFa on these rows, so the markup
   is the data representation. Every selector below stays CLASS-keyed, never
   element-keyed: ResourceForm.jsx's whole-form mode reuses these same classes
   on divs, because a form body is not a description list. */
.ldh-prop-form {
  display: flex; flex-direction: column;
  /* Cancels the UA block margin now that read mode is a <dl>; harmless on the
     form mode's <div>. No <dd> reset is needed — .ldh-prop-row is
     display:contents, so it generates no box for margin-inline-start to hit. */
  margin: 0;
}

/* One predicate (with N values) lives in a .ldh-prop-group grid. The label
   cell spans N rows; each value's .ldh-prop-row uses display:contents so its
   value/type/actions cells slot into columns 2/3/4 of the parent grid. */
.ldh-prop-group {
  display: grid;
  grid-template-columns: 200px 1fr 64px;
  /* MUST stay 0 — see the statement-delimiter block below. */
  column-gap: 0;
}

/* Shared label cell. Spans all value rows when there are >1; align text to
   the right and vertically center across the spanned rows. */
.ldh-prop-group > .label {
  position: relative;
  padding: var(--sp-3) var(--sp-3) var(--sp-3) var(--sp-6);
  text-align: right;
  line-height: 1.25;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-self: stretch;
}
/* No predicate CURIE in the label column. The label column shows the human
   label only; the machine name is the group's identity in the data, reachable
   through the rdfs:comment toggletip and the row's own actions. */
.ldh-prop-group > .label .pred {
  color: var(--fg-2);
  font-family: var(--font-sans);
  font-weight: var(--fw-medium);
  font-size: var(--fs-sm);
}

/* Value row — display:contents puts its cells (value, row-actions)
   directly into the parent grid's columns 2, 3. */
.ldh-prop-row { display: contents; }
.ldh-prop-row > .value,
.ldh-prop-row > .row-actions {
  /* MUST stretch: these share a grid row with the (spanning) label cell, and
     the statement hairline + hover band are drawn on all three. A cell that
     sizes to its content draws its rule at a different y than its neighbours. */
  align-self: stretch;
  padding-top: var(--sp-3);
  padding-bottom: var(--sp-3);
  padding-left: var(--sp-3);
  transition: background var(--dur-fast) var(--ease-out-soft);
}
.ldh-prop-row > .row-actions { padding-right: var(--sp-6); }

/* =========================================================================
   STATEMENT DELIMITERS — a hairline under each statement, as in the product.
   .ldh-prop-row is display:contents, so the rule has to go on the CELLS; put
   it on all three and they line up into one continuous rule.

   The label cell SPANS a multi-value group, so its border draws once at the
   bottom of the whole span while .value / .row-actions draw per value. That
   falls out exactly right: a single-value statement gets one full-width rule,
   and extra values of the same predicate are separated only across columns
   2–3 — the unbroken label column reads as "still the same predicate".

   The final statement has no rule: the card edge already ends the list, and a
   trailing hairline reads as a dangling separator.
   ========================================================================= */
.ldh-prop-form .ldh-prop-group > .label,
.ldh-prop-form .ldh-prop-row > .value,
.ldh-prop-form .ldh-prop-row > .row-actions { border-bottom: 1px solid var(--border-default); }
.ldh-prop-form > .ldh-prop-group:last-child > .label,
.ldh-prop-form > .ldh-prop-group:last-child .ldh-prop-row:last-child > .value,
.ldh-prop-form > .ldh-prop-group:last-child .ldh-prop-row:last-child > .row-actions { border-bottom: 0; }
.ldh-prop-group:hover > .label,
.ldh-prop-row:hover > .value,
.ldh-prop-row:hover > .row-actions {
  background: var(--surface-1);
}
/* Bridge the grid column-gaps so the hover highlight reads as one continuous
   band rather than three segments separated by white gaps. Each end cell
   extends only toward the value; the value extends both ways. */
/* No bridging shadows needed now that column-gap is 0 — the three cells are
   adjacent, so their hover backgrounds already form one continuous band. */

/* =========================================================================
   NARROW CONTAINER — the label stops being a column and becomes a line.

   200px of label + 64px of actions is 264px of fixed track. In a 290px block
   that left the VALUE - the data the page exists to show - 26px, and every
   word broke to one character per line; in the 300px content aside it is 36px
   at a full 1440px desktop, which is why this is a container query and not a
   media query. The label keeps its column down to ~520px, where 200 + 240 of
   readable value + 64 stops fitting.

   Collapsed, the label spans the row above its values. The three cells that
   carry the statement hairline become two, and the label must NOT keep its
   border: spanning the full width, its rule would land between a predicate and
   its own value and read as a separator where there is no boundary. The
   multi-value behaviour survives the change - the label is stated once above
   the group, and only the value rows are delimited from each other, which is
   the same "still the same predicate" reading the unbroken label column gave.
   ========================================================================= */
@container (max-width: 520px) {
  .ldh-prop-group { grid-template-columns: minmax(0, 1fr) auto; }
  .ldh-prop-group > .label {
    grid-column: 1 / -1;
    text-align: left;
    padding: var(--sp-3) var(--sp-4) 0;
    /* justify-content: center is for a cell stretched across spanned rows; on its own row it
       has nothing to centre against. */
    justify-content: flex-start;
  }
  .ldh-prop-form .ldh-prop-group > .label { border-bottom: 0; }
  .ldh-prop-row > .value { padding-left: var(--sp-4); }
  .ldh-prop-row > .row-actions { padding-right: var(--sp-4); }
}

/* Value cell — looks like plain text in read mode. Clickable interactive rows
   get a subtle hover affordance. Inputs only appear when editing. */
.ldh-prop-row .value {
  min-height: 32px;
  font-size: var(--fs-sm);
  color: var(--fg-1);
  border: 1px solid transparent;
  background: transparent;
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  min-width: 0;
  transition: background var(--dur-fast) var(--ease-out-soft), border-color var(--dur-fast) var(--ease-out-soft);
  cursor: text;
  word-break: break-word;
}
.ldh-prop-row.is-interactive .value:hover {
  background: var(--surface-1);
}
.ldh-prop-row.is-interactive .value:focus-visible {
  outline: 0;
  background: var(--surface-2);
  border-color: var(--ldh-blue-300);
  box-shadow: var(--shadow-glow-blue);
}
.ldh-prop-row .value .long { white-space: pre-wrap; }

/* Edit state — input fills the value cell with a distinct (recessed) surface so
   the active field reads as separate from the white card around it. */
.ldh-prop-row.is-editing .value {
  background: var(--surface-2);
  border-color: var(--ldh-blue-300);
  /* the delimiter is the list's, not the field's — keep it through editing */
  border-bottom-color: var(--border-default);
  box-shadow: var(--shadow-glow-blue);
  padding: 4px 6px;
}
.ldh-prop-row.is-editing .value input,
.ldh-prop-row.is-editing .value textarea,
.ldh-prop-row.is-editing .value select {
  flex: 1; min-width: 0;
  background: transparent;
  border: 0;
  outline: 0;
  padding: 4px 4px;
  font: inherit;
  color: inherit;
  resize: vertical;
  line-height: 1.5;
}
.ldh-prop-row.is-editing .value textarea {
  min-height: 80px;
}

/* Resource typeahead picker — used in place of the plain input when editing
   a resource-valued cell. Composed of a small search input followed by an
   inline dropdown list of dataspace matches. */
.ldh-resource-pick {
  position: relative;
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.ldh-resource-pick-input {
  display: flex; align-items: center; gap: 6px;
  height: 32px;
  padding: 0 10px;
  border-radius: var(--r-sm);
  border: 1px solid var(--ldh-blue-400);
  background: var(--surface-0);
  box-shadow: 0 0 0 3px var(--bg-accent-quiet);
}
.ldh-resource-pick-input .msi { color: var(--fg-2); flex-shrink: 0; }
.ldh-resource-pick-input input {
  flex: 1;
  min-width: 0;
  border: 0;
  outline: 0;
  background: transparent;
  font: inherit;
  color: var(--fg-1);
}
.ldh-resource-pick-input input::placeholder { color: var(--fg-hint); }

.ldh-resource-pick-list {
  position: absolute;
  top: calc(32px + 6px);
  left: 0;
  right: 0;
  background: var(--bg-card);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  box-shadow: 0 16px 40px -12px rgba(15, 23, 42, 0.28), 0 4px 12px -4px rgba(15, 23, 42, 0.18);
  padding: 4px;
  max-height: 320px;
  overflow: auto;
  z-index: 20;
  animation: menu-pop var(--dur-fast) var(--ease-out-soft);
}
.ldh-resource-pick-section {
  padding: 6px 10px 4px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--fg-muted);
}
.ldh-resource-pick-item {
  display: grid;
  grid-template-columns: 22px 1fr auto;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 6px 10px;
  border: 0;
  background: transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
  font: inherit;
  text-align: left;
  color: var(--fg-1);
}
.ldh-resource-pick-item:hover,
.ldh-resource-pick-item.is-active {
  background: var(--surface-1);
}
.ldh-resource-pick-item .ico {
  width: 22px; height: 22px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-sm);
  background: var(--bg-accent-quiet);
  color: var(--ldh-blue-700);
  flex-shrink: 0;
}
.ldh-resource-pick-item .body {
  display: flex; flex-direction: column;
  min-width: 0;
}
.ldh-resource-pick-item .body .label {
  font-size: var(--fs-sm);
  color: var(--fg-1);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ldh-resource-pick-item .body .iri {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--fg-hint);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ldh-resource-pick-item .cls {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--ldh-blue-700);
  background: var(--bg-accent-quiet);
  padding: 1px 6px;
  border-radius: var(--r-pill);
  white-space: nowrap;
  flex-shrink: 0;
}
.ldh-resource-pick-empty {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 12px 10px;
  color: var(--fg-2);
  font-size: var(--fs-sm);
}
.ldh-resource-pick-empty .hint {
  font-size: 11px;
  color: var(--fg-hint);
  margin-top: 2px;
}

/* Non-interactive value display variants */
.ldh-prop-row .value .chip-inline {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px; border-radius: var(--r-pill);
  background: var(--ldh-violet-50);
  color: var(--ldh-violet-700);
  font-family: var(--font-sans); font-size: 12px; font-weight: var(--fw-medium);
}
.ldh-prop-row .value .image-mini {
  display: inline-flex; align-items: center; gap: 8px;
}
.ldh-prop-row .value .image-mini .thumb {
  width: 24px; height: 24px; border-radius: 4px;
  background: var(--surf-peach);
  display: inline-flex; align-items: center; justify-content: center;
  color: light-dark(#c25c00, #f0b27a);
}
.ldh-prop-row .value .iri-link {
  font-family: var(--font-mono);
  color: var(--ldh-blue-700);
}

/* Right-side row actions — edit (interactive rows) + remove.
   The reveal is on the BUTTONS, not on the cell: the cell carries the statement
   delimiter, and fading the cell faded its border too, so the hairline stopped
   short of the actions column. Fading the controls keeps the cell (and its
   rule) present while the row is at rest, and the footprint never shifts. */
.ldh-prop-row .row-actions {
  display: flex; gap: 2px;
  justify-content: flex-end;
  align-items: center;
}
.ldh-prop-row .row-actions > * {
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-out-soft);
}
.ldh-prop-row:hover .row-actions > *,
.ldh-prop-row.is-editing .row-actions > *,
.ldh-prop-row:focus-within .row-actions > * { opacity: 1; }
/* ALWAYS-ON API. Because the reveal moved from the cell to its children, the
   old `.row-actions { opacity: 1 }` override silently stopped working — it now
   sets a cell that is already opaque and never reaches the buttons. Anything
   that needs the affordances permanently visible (documentation specimens, a
   touch-only surface with no hover) must use one of these instead of setting
   opacity on the cell:
     · .row-actions.is-always-on   — one row
     · .ldh-prop-form.is-specimen  — every row in the form
   Cell-level opacity is deliberately no longer a knob: it hides the statement
   delimiter drawn on that cell along with the buttons. */
.ldh-prop-row .row-actions.is-always-on > *,
.ldh-prop-form.is-specimen .ldh-prop-row .row-actions > * { opacity: 1; }

/* Row add/remove are <IconButton size="xs" intent="accent|destructive"> — the
   primitive already carries the 28px box and both hover tints. */

/* Add-row footer — single block-level affordance that survived the row-level refactor */
.ldh-prop-addrow {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-6);
  background: var(--surface-1);
  border-top: 1px dashed var(--border-default);
}
.ldh-prop-addrow .pred-pick {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  height: 32px; padding: 0 var(--sp-3);
  background: var(--surface-0);
  border: 1px solid var(--border-default);
  border-radius: var(--r-sm);
  font-size: var(--fs-sm);
  color: var(--fg-2);
  font-family: var(--font-mono);
}
.ldh-prop-addrow .add-prop {
  width: 32px; height: 32px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--ldh-blue-50); color: var(--ldh-blue-700);
  border: 1px solid var(--ldh-blue-100);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out-soft);
}
.ldh-prop-addrow .add-prop:hover { background: var(--ldh-blue-100); }

/* RDF type tag — small pill at right of input */
.ldh-rdf-type {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 8px;
  border-radius: var(--r-pill);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: var(--fw-medium);
  letter-spacing: 0.02em;
  text-transform: lowercase;
  border: 1px solid transparent;
  justify-self: start;
  white-space: nowrap;
}

/* In a property row the rdf-type pill is no longer shown — the term type is
   conveyed by the value rendering itself (links for resources, plain text for
   literals). The .ldh-rdf-type component below is still used by the RDF-tags
   card and the graph detail panel. */
.ldh-rdf-type.class    { color: var(--ldh-violet-700); background: var(--ldh-violet-50); border-color: var(--ldh-violet-100); }
.ldh-rdf-type.resource { color: var(--ldh-blue-700);   background: var(--ldh-blue-50);   border-color: var(--ldh-blue-100); }
/* Three chips whose fills follow the scheme were wearing three edges that do not:
   each border, and the named chip's fill, is a literal. The dark side therefore
   derives from the tokens the chip already sits on — the fill carried a step
   toward its own ink — while the light side keeps its literal untouched.
   Deriving light the same way is the obvious tidy-up and deliberately NOT done
   here: these literals were tuned against the BASE palette, and the shipped m3
   skin remaps success/warning/violet, so the mix lands a visibly deeper edge
   (#c8eedb becomes rgb(162,222,182)). That is a light-scheme redesign of a
   component, not part of making it work in dark, so it is left to ask. */
.ldh-rdf-type.literal  { color: var(--success-500);    background: var(--success-50);    border-color: light-dark(#c8eedb, color-mix(in srgb, var(--success-500) 15%, var(--success-50))); }
.ldh-rdf-type.blank    { color: var(--warning-500);    background: var(--warning-50);    border-color: light-dark(#f6e1ad, color-mix(in srgb, var(--warning-500) 20%, var(--warning-50))); }
.ldh-rdf-type.named    { color: var(--ldh-violet-700); background: light-dark(#f4ecfb, var(--ldh-violet-100)); border-color: light-dark(#e1cdf6, color-mix(in srgb, var(--ldh-violet-500) 22%, var(--ldh-violet-100))); }
.ldh-rdf-type::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; opacity: 0.85;
}

/* ---------- Text block (XHTML embedded content) ---------- */
/* No own padding: it sits inside .ldh-block-body, which already provides the
   block's content inset. Re-padding here double-indented the prose past the
   block header and the other blocks (charts, etc.). */
.ldh-text-block { padding: 0; }
.ldh-text-block p { margin: 0 0 var(--sp-3); color: var(--ink-2); line-height: var(--lh-base); font-size: var(--fs-base); }
.ldh-text-block p:last-child { margin-bottom: 0; }
.ldh-text-block a { color: var(--fg-link); }

/* ---------- Empty state for misc ---------- */
.ldh-empty {
  padding: var(--sp-6); text-align: center; color: var(--fg-hint); font-size: var(--fs-sm);
}

/* =========================================================================
   BLOCK STATES — failed / empty / loading.

   DECISION: a block KEEPS its card chrome and header in every state; only the
   BODY swaps. It does not collapse to a bare alert. Three reasons:
     1. A block is a persistent addressable object — it has an IRI and block
        actions (copy IRI, edit query, delete). Collapsing to an alert strips
        exactly the affordances the failure case needs most.
     2. The title comes from RDF and is known before the query resolves, so
        there is nothing to gain by hiding it while loading.
     3. Layout stability — the card holds its footprint, so the document does
        not reflow as blocks resolve.
   The header gains one status marker so the state is legible when the block is
   scrolled past or collapsed.
   ========================================================================= */

/* Header status marker — sits after the title, before the actions. */
.ldh-block-state {
  display: inline-flex; align-items: center; gap: 4px;
  margin-left: var(--sp-2); padding: 1px 8px;
  border-radius: var(--r-pill); flex-shrink: 0;
  font-family: var(--font-mono); font-size: var(--fs-xs); font-weight: var(--fw-medium);
  white-space: nowrap; vertical-align: middle;
}
.ldh-block-state .msi { font-size: 13px; }
.ldh-block-state.is-failed  { background: var(--danger-50);  color: var(--danger-500); }
.ldh-block-state.is-empty   { background: var(--surface-2);  color: var(--fg-muted); }
.ldh-block-state.is-loading { background: var(--surface-2);  color: var(--fg-muted); }
.ldh-block-state.is-loading .msi { animation: spin 1.1s linear infinite; }
/* A failed block gets a warm ring so it's findable in a long document. This is
   an `outline`, not a border: skins differ on whether cards even have borders
   (m3 uses box-shadow elevation and sets border-width: 0), and an outline
   survives both without fighting the skin's elevation shadow. Offset inward so
   it lands on the card edge rather than outside it. */
.ldh-block.is-failed {
  outline: 1px solid color-mix(in srgb, var(--danger-500) 38%, transparent);
  outline-offset: -1px;
  border-color: color-mix(in srgb, var(--danger-500) 28%, var(--border-default));
}

/* Error body: the alert owns the message; the raw detail is demoted. */
.ldh-block-error { display: flex; flex-direction: column; gap: var(--sp-3); }
/* The technical detail the error body demotes is the core kit's Disclosure
   (.ac-disclosure): a collapsed <details> is not block vocabulary, and the
   client layer renders one too. */

/* Empty body: literal, quiet, with the one action that resolves it. Per the
   copy rules — "No results." not "Nothing here yet 👀". */
.ldh-block-blank {
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
  padding: var(--sp-8) var(--sp-4); text-align: center;
}
.ldh-block-blank > .msi { font-size: 28px; color: var(--ink-5); }
.ldh-block-blank .bb-msg { font-size: var(--fs-base); color: var(--fg-2); font-weight: var(--fw-medium); }
.ldh-block-blank .bb-sub { font-size: var(--fs-sm); color: var(--fg-hint); max-width: 46ch; line-height: var(--lh-base); }
.ldh-block-blank .bb-act { margin-top: var(--sp-2); }

/* Loading body: skeleton shaped like the content it replaces. */
.ldh-block-loading { display: flex; flex-direction: column; gap: var(--sp-3); }
.ldh-block-loading .bl-row { display: flex; align-items: center; gap: var(--sp-3); }
/* Core's .ac-skel-group is width:100% (it's normally a block-level stack), so
   a fixed-width skeleton used as a flex child still claims a 100% flex base and
   over-subscribes the row — leaving no free space for a flex:1 sibling to grow
   into, which collapses it to 0. Pin the fixed columns to their content width so
   the fluid column keeps the remainder. */
.ldh-block-loading .bl-row > .ac-skel-group { flex: 0 0 auto; width: auto; }

/* ---------- View-block toolbar (facets / sort / count, sits inside .ldh-block between head and body) ---------- */
.ldh-view-toolbar {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-6);
  border-bottom: 1px dashed var(--border-default);
  background: var(--surface-1);
  flex-wrap: wrap;
}
.ldh-view-toolbar .left { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.ldh-view-toolbar .right { display: flex; align-items: center; gap: var(--sp-2); margin-left: auto; flex-wrap: wrap; }
/* The toolbar itself wraps, but its end cluster did not, so the mode switcher, pager and count
   travelled as one 411-475px unit and left the block together. margin-left: auto is what pins that
   unit to the far edge; once the group is the only thing on its line there is no far edge to pin to,
   and keeping it turns a wrapped row into a right-aligned orphan. */
@container (max-width: 520px) {
  .ldh-view-toolbar .right { margin-left: 0; }
}
.ldh-view-toolbar .count {
  font-family: var(--font-mono); font-size: var(--fs-xs);
  color: var(--fg-muted); white-space: nowrap;
}
.ldh-view-toolbar .count b { color: var(--fg-1); font-weight: var(--fw-semibold); }

/* Facet pills — read like Linear/Notion filter chips. Compact, removable. */
.facet-pill {
  display: inline-flex; align-items: center; gap: 6px;
  height: 28px; padding: 0 10px;
  background: var(--surface-0);
  border: 1px solid var(--border-default);
  border-radius: var(--r-pill);
  font-family: var(--font-sans); font-size: var(--fs-sm);
  color: var(--fg-2);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out-soft), border-color var(--dur-fast) var(--ease-out-soft);
  white-space: nowrap;
}
.facet-pill:hover { border-color: var(--border-strong); background: var(--surface-2); }
.facet-pill .pred {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.02em;
  color: var(--fg-hint); text-transform: lowercase;
}
.facet-pill .val { font-weight: var(--fw-medium); color: var(--fg-1); }
.facet-pill.is-active { background: var(--ldh-blue-50); color: var(--ldh-blue-700); border-color: var(--ldh-blue-200); }
.facet-pill.is-active .pred { color: var(--ldh-blue-600); }
.facet-pill.is-active .val { color: var(--ldh-blue-700); }
.facet-pill .x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: 50%;
  color: var(--fg-muted);
  margin-left: 2px;
}
.facet-pill .x:hover { background: color-mix(in srgb, var(--fg-1) 6%, transparent); color: var(--fg-1); }
.facet-pill .caret { color: var(--fg-muted); margin-left: 1px; }
.facet-pill.is-open { border-color: var(--ldh-blue-300); background: var(--surface-2); }
.facet-pill.add {
  border-style: dashed; color: var(--fg-muted); background: transparent;
}
.facet-pill.add:hover { color: var(--fg-1); border-color: var(--border-strong); }
/* Sort direction toggle — a .facet-pill so it matches the bar, but it states a
   direction rather than a filter, so the arrow carries the accent and the pill
   itself stays neutral. */
.facet-pill.sort-dir { gap: 4px; }
.facet-pill.sort-dir .msi { color: var(--ldh-blue-600); }
.facet-pill.sort-dir:hover .msi { color: var(--ldh-blue-700); }
.facet-pill.sort-dir .val { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.02em; }

/* Filters lead icon + clear-all */
/* Each control row opens with a glyph naming it - the funnel for the filters, the
   junction for the pivots. One rule for both, because the point of the second one is
   that it lands exactly where the first does down the card's left edge. */
.ldh-view-toolbar .facet-lead, .ldh-pivot-bar .pivot-lead { display: inline-flex; align-items: center; color: var(--fg-hint); margin-right: 2px; }
.facet-clear-all {
  border: 0; background: transparent; cursor: pointer;
  font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: var(--fw-medium);
  color: var(--fg-link); padding: 0 6px; height: 28px;
}
.facet-clear-all:hover { text-decoration: underline; }

/* Facet popover (anchored to a pill) */
.facet-pop {
  position: absolute; top: calc(100% + 6px); left: 0;
  min-width: min(240px, calc(100vw - 2 * var(--sp-4)));
  background: var(--surface-0); border: 1px solid var(--border-default);
  border-radius: var(--r-md); box-shadow: var(--shadow-lg);
  padding: var(--sp-2);
  z-index: 40;
}
.facet-pop .opt {
  display: flex; align-items: center; gap: var(--sp-2);
  width: 100%; text-align: left;
  padding: 6px var(--sp-2);
  border: 0; background: transparent;
  border-radius: var(--r-sm);
  font-family: inherit; font-size: var(--fs-sm);
  color: var(--fg-2);
  cursor: pointer;
}
.facet-pop .opt:hover { background: var(--bg-hover); color: var(--fg-1); }
.facet-pop .opt .check {
  width: 14px; height: 14px; border-radius: 3px;
  border: 1.5px solid var(--border-strong);
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--fg-on-accent); background: var(--surface-0);
  flex-shrink: 0;
}
.facet-pop .opt.is-on .check { background: var(--ldh-blue-500); border-color: var(--ldh-blue-500); }
.facet-pop .opt .nm { flex: 1; }
.facet-pop .opt .cnt { font-family: var(--font-mono); font-size: 10px; color: var(--fg-hint); }
.facet-pop .facet-values { display: flex; flex-direction: column; gap: 1px; max-height: 264px; overflow: auto; }
.facet-pop .facet-empty { padding: 8px var(--sp-2); font-size: var(--fs-xs); color: var(--fg-muted); }
/* Sort menu — single-selection, so no leading checkbox. The chosen key is the
   highlighted row plus a trailing tick; the facet menu's .check box is its
   multi-select affordance and would misdescribe this one. */
.facet-pop.sort-pop .opt.is-on { background: var(--bg-selected); color: var(--fg-selected); font-weight: var(--fw-medium); }
.facet-pop.sort-pop .opt .tick { margin-left: auto; color: var(--fg-selected); flex-shrink: 0; }
.facet-pop .head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  padding: 6px var(--sp-2) 6px;
  border-bottom: 1px solid var(--border-default);
  margin-bottom: 4px;
}
.facet-pop .head .pname {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.02em;
  text-transform: uppercase; color: var(--fg-muted);
}
.facet-pop .head .clear {
  border: 0; background: transparent; cursor: pointer;
  font-family: var(--font-sans); font-size: 11px; font-weight: var(--fw-medium);
  color: var(--fg-link); padding: 0;
}
.facet-pop .head .clear:hover { text-decoration: underline; }

/* Two-option list/grid toggle in the sidebar's Latest-documents modal. NOT the
   view's layout switcher any more — five options in a strip was too much
   toolbar for too little; that is <ModeSwitcher size="sm"> now. A segmented
   control still earns its space at two options. */
.view-mode-seg {
  display: inline-flex; align-items: center;
  background: var(--surface-0);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  padding: 2px;
}
.view-mode-seg button {
  width: 28px; height: 24px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 0; border-radius: 4px;
  color: var(--fg-muted);
}
.view-mode-seg button:hover { background: var(--bg-hover); color: var(--fg-1); }
.view-mode-seg button.is-on { background: var(--bg-accent-quiet); color: var(--ldh-blue-700); }

/* The sort control is <SortControl>: a .facet-pill key dropdown plus a
   .facet-pill.sort-dir direction button. The old static .sort-drop div (a
   hardcoded arrow that opened nothing) is gone. */

/* ---------- GridViewBlock — cards grid ---------- */
.ldh-grid-block {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: var(--sp-4);
  padding: var(--sp-5) var(--sp-6);
}
/* When a view block sits inside the standard <Block> shell, its .ldh-block-body
   already provides the 24px content inset (titles align to it). The grid and the
   view-toolbar must NOT re-pad horizontally or they double it (cards landed at
   ~48px while the block title sat at 24px). Drop the redundant horizontal inset
   and the grid's doubled bottom padding so everything aligns to the title. */
.ldh-block-body > .ldh-grid-block { padding: var(--sp-2) 0 0; }
.ldh-block-body > .ldh-view-toolbar { padding-left: 0; padding-right: 0; }
/* The pivot bar is the toolbar's sibling and must take the same treatment: the
   block body already supplies the --sp-6 inset, so keeping its own would push
   the pivot pills 24px right of the facet pills. The bar itself carries no
   padding — its summary and its pill row do, so both are named here. */
.ldh-block-body > .ldh-pivot-bar { padding-left: 0; padding-right: 0; }
/* A block's control bands are rendered carrying is-collapsed and the card header's
   controls toggle is what clears it - the same token the query block's editor pane
   uses. A surface with no such header (a document in ChartMode, a modal-hosted view)
   never gets the token, because nothing there could ever clear it. */
.ldh-view-toolbar.is-collapsed,
.ldh-pivot-bar.is-collapsed,
.chart-controls.is-collapsed { display: none; }
.ldh-grid-block .card {
  position: relative;
  display: flex; flex-direction: column; align-items: stretch;
  background: var(--bg-card);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  overflow: hidden;
  text-decoration: none; color: inherit;
  transition: border-color var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-fast) var(--ease-out-soft), transform var(--dur-fast) var(--ease-out-soft);
}
.ldh-grid-block .card:hover {
  border-color: var(--ldh-blue-300);
  box-shadow: var(--shadow-sm);
  transform: translateY(-2px);
  text-decoration: none;
}
.ldh-grid-block .card .img {
  aspect-ratio: 4 / 3;
  display: flex; align-items: center; justify-content: center;
  color: rgba(0,0,0,0.45);
}
.ldh-grid-block .card .img .msi { font-size: 56px; }
/* The slot is a flex item of the column-flex .card, so its min-height: auto
   resolves to the image's transferred content size and outvotes aspect-ratio —
   a 1:3.5 source blows the card out to the image's height. min-height: 0 is the
   minimal guard; the image then fits the box instead of sizing it, so every card
   in a grid is the same height whatever the sources are.
   contain is the default: in a generic RDF system a depiction is as often an
   emblem, diagram or logo as a photograph, and cropping one loses the subject.
   .fit-cover opts in, mirroring <image-slot>'s own fit vocabulary. */
.ldh-grid-block .card .img { min-height: 0; }
.ldh-grid-block .card .img img { display: block; width: 100%; height: 100%; object-fit: contain; }
.ldh-grid-block .card .img.fit-cover img { object-fit: cover; }
.ldh-grid-block .card .card-body { padding: var(--sp-3) var(--sp-4); display: flex; flex-direction: column; gap: 4px; }
.ldh-grid-block .card .ti { display: block; font-weight: var(--fw-semibold); color: var(--ldh-blue-700); font-size: var(--fs-base); line-height: var(--lh-snug); }
.ldh-grid-block .card .meta { display: block; font-size: var(--fs-xs); color: var(--fg-muted); line-height: var(--lh-base); }
.ldh-grid-block .card .pin {
  position: absolute; top: var(--sp-2); right: var(--sp-2);
  display: flex; gap: 2px;
  opacity: 0; transition: opacity var(--dur-fast) var(--ease-out-soft);
}
.ldh-grid-block .card:hover .pin { opacity: 1; }
/* Decorative icon slots inside a card link — they cannot be buttons (nested
   interactive content), so they get their own class rather than borrowing one. */
.ldh-grid-block .card .pin .ldh-pin-ic {
  width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--surface-0) 92%, transparent); backdrop-filter: blur(6px);
  border: 1px solid var(--border-default); border-radius: var(--r-sm);
  color: var(--fg-2);
  display: inline-flex; align-items: center; justify-content: center;
}

/* Pastel image fills for category cards */
.img-lavender { background: var(--surf-lavender); }
.img-mint     { background: var(--surf-mint); }
.img-peach    { background: var(--surf-peach); }
.img-sky      { background: var(--surf-sky); }
.img-blush    { background: var(--surf-blush); }
.img-sand     { background: var(--surf-sand); }
.img-mesh     { background: var(--grad-aurora); }

/* ---------- ListViewBlock — rows of resources ---------- */
.ldh-list-block { display: flex; flex-direction: column; }
.ldh-list-block .row {
  display: grid; grid-template-columns: 28px 1fr auto auto;
  align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-6);
  border-bottom: 1px solid var(--border-default);
  text-decoration: none; color: inherit;
  transition: background var(--dur-fast) var(--ease-out-soft);
}
.ldh-list-block .row:last-child { border-bottom: 0; }
.ldh-list-block .row:hover { background: var(--surface-1); text-decoration: none; }
.ldh-list-block .row .ti {
  font-weight: var(--fw-medium); color: var(--ldh-blue-700); font-size: var(--fs-base);
}
.ldh-list-block .row .desc { display: block; font-size: var(--fs-sm); color: var(--fg-muted); font-weight: 400; margin-top: 1px; }
.ldh-list-block .row .ic {
  width: 28px; height: 28px; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--fg-muted); background: var(--surface-2);
}
.ldh-list-block .row .ts {
  font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--fg-hint); white-space: nowrap;
}
/* An rdf:type value renders as the core type Tag (.ac-tag.em-quiet.co-primary)
   wherever it lands — property values, table cells and list rows alike. The
   `.type` cell only PLACES that Tag; it paints no pill of its own. Only the
   title-line `a.ldh-type-chip` stays distinct, as the linked variant.
   `.ldh-type-chip` here is the OUTGOING markup — the shipped bundle still
   emits it until recompile, so it keeps its placement rule. */
.ldh-list-block .row .type { display: inline-flex; flex-shrink: 0; }
.ldh-list-block .row .ldh-type-chip { flex-shrink: 0; }
/* The timestamp is white-space: nowrap and the type tag is flex-shrink: 0, so columns 3 and 4 are
   incompressible by design - correct, and the reason the row cannot simply be squeezed. Narrow, the
   metadata drops to a second line under the title and the title takes the full remaining width.
   All four cells are placed explicitly: with only some of them placed, auto-placement backfills the
   others into whatever cell is free next, which is how a type tag ends up beneath the icon. */
@container (max-width: 520px) {
  .ldh-list-block .row { grid-template-columns: 28px minmax(0, 1fr) auto; row-gap: 2px; padding-left: var(--sp-4); padding-right: var(--sp-4); }
  .ldh-list-block .row > .ic { grid-row: 1; grid-column: 1; align-self: start; }
  .ldh-list-block .row > .ti { grid-row: 1; grid-column: 2 / -1; }
  .ldh-list-block .row > .ts { grid-row: 2; grid-column: 2; justify-self: start; }
  .ldh-list-block .row > .type { grid-row: 2; grid-column: 3; justify-self: end; }
}

/* ---------- Full-bleed mode wrappers (Graph / Map) ---------- */
.ldh-fullbleed {
  position: relative;
  height: calc(100vh - 56px - 40px - 64px - 64px);   /* viewport - header - tabs - actionbar - some breathing */
  min-height: 520px;
  border-radius: var(--r-lg);
  overflow: hidden;
  border: 1px solid var(--border-default);
}
.ldh-fullbleed .overlay {
  position: absolute;
  z-index: 5;
  background: color-mix(in srgb, var(--surface-0) 92%, transparent);
  backdrop-filter: blur(8px);
  border: 1px solid var(--border-default);
  border-radius: var(--r-md);
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-sm); color: var(--fg-2);
  box-shadow: var(--shadow-sm);
}
.ldh-fullbleed .overlay.on-dark {
  background: rgba(8, 14, 32, 0.7);
  border-color: rgba(255,255,255,0.10);
  color: rgba(255,255,255,0.92);
}
.ldh-fullbleed .overlay.tl { top: var(--sp-3); left: var(--sp-3); }
.ldh-fullbleed .overlay.tr { top: var(--sp-3); right: var(--sp-3); }
.ldh-fullbleed .overlay.bl { bottom: var(--sp-3); left: var(--sp-3); }
.ldh-fullbleed .overlay.bc { bottom: var(--sp-3); left: 50%; transform: translateX(-50%); }

/* Graph viewer */
.graph-canvas {
  position: absolute; inset: 0;
  background:
    radial-gradient(circle at 1px 1px, rgba(255,255,255,0.10) 1px, transparent 1.4px) 0 0 / 22px 22px,
    radial-gradient(60% 60% at 30% 20%, rgba(77,148,248,0.22) 0%, transparent 60%),
    radial-gradient(50% 50% at 80% 70%, rgba(153,84,187,0.20) 0%, transparent 65%),
    linear-gradient(180deg, #0a1230 0%, #141d3d 100%);
}
.graph-canvas svg { width: 100%; height: 100%; display: block; }
.graph-canvas .node-label {
  position: absolute; transform: translate(-50%, -50%);
  font-family: var(--font-mono); font-size: 10px;
  color: rgba(255,255,255,0.85);
  background: rgba(8, 14, 32, 0.6);
  padding: 1px 6px; border-radius: 3px;
  pointer-events: none; white-space: nowrap;
}
.graph-filters .row {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 4px 0;
  font-size: var(--fs-sm); color: rgba(255,255,255,0.85);
  cursor: pointer;
}
.graph-filters .row .check {
  width: 14px; height: 14px; border-radius: 3px;
  border: 1.5px solid rgba(255,255,255,0.45);
  display: inline-flex; align-items: center; justify-content: center;
  color: #0a1230;
}
.graph-filters .row.is-on .check { background: var(--ldh-blue-400); border-color: var(--ldh-blue-400); }
.graph-filters .row.is-disabled { opacity: 0.45; }

.graph-zoom {
  display: flex; flex-direction: column; gap: 2px;
  padding: 2px;
}
.graph-zoom button {
  width: 32px; height: 28px;
  background: transparent; border: 0; color: var(--fg-2);
  border-radius: 4px;
  display: inline-flex; align-items: center; justify-content: center;
}
.graph-zoom button + button { border-top: 1px solid var(--border-default); border-radius: 0; }
.graph-zoom button:hover { background: var(--surface-2); }

/* Selected-node side panel (right-side floating) */
.graph-detail {
  position: absolute; top: var(--sp-3); right: var(--sp-3); bottom: var(--sp-3);
  width: 280px;
  background: rgba(8, 14, 32, 0.84);
  backdrop-filter: blur(12px);
  color: rgba(255,255,255,0.92);
  border: 1px solid rgba(255,255,255,0.10);
  border-radius: var(--r-md);
  padding: var(--sp-4);
  display: flex; flex-direction: column; gap: var(--sp-3);
  font-size: var(--fs-sm);
  z-index: 6;
}
.graph-detail .iri { font-family: var(--font-mono); font-size: var(--fs-xs); color: rgba(255,255,255,0.65); word-break: break-all; }
.graph-detail .h { font-weight: var(--fw-semibold); color: #fff; }
.graph-detail dl { display: grid; grid-template-columns: 96px 1fr; gap: 4px var(--sp-3); margin: 0; }
.graph-detail dt { font-family: var(--font-mono); font-size: 10px; color: rgba(255,255,255,0.55); text-align: right; letter-spacing: 0.02em; }
.graph-detail dd { margin: 0; color: rgba(255,255,255,0.92); }

/* Map canvas */
.map-canvas {
  position: absolute; inset: 0;
  background:
    radial-gradient(ellipse at 30% 40%, #f5edd9 0%, #f5edd9 12%, transparent 14%),
    radial-gradient(ellipse at 55% 50%, #f5edd9 0%, #f5edd9 18%, transparent 20%),
    radial-gradient(ellipse at 75% 35%, #f5edd9 0%, #f5edd9 10%, transparent 12%),
    radial-gradient(ellipse at 65% 70%, #f5edd9 0%, #f5edd9 14%, transparent 16%),
    linear-gradient(180deg, #d6e7f5 0%, #c4dbef 100%);
  background-size: 200% 180%, 240% 200%, 180% 160%, 200% 180%, 100% 100%;
  background-position: -40% 30%, 25% 20%, 60% -10%, 40% 60%, 0 0;
  background-repeat: no-repeat;
}
.map-canvas::before {
  content: "";
  position: absolute; inset: 0;
  background-image:
    linear-gradient(0deg, transparent 49%, rgba(78, 128, 178, 0.12) 50%, transparent 51%),
    linear-gradient(90deg, transparent 49%, rgba(78, 128, 178, 0.12) 50%, transparent 51%);
  background-size: 80px 80px;
  opacity: 0.5;
}
.map-pin {
  position: absolute;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--ldh-blue-500);
  border: 3px solid #fff;
  box-shadow: 0 4px 10px -2px rgba(15,23,42,0.45);
  transform: translate(-50%, -100%);
}
.map-pin.warn { background: var(--warning-500); }
.map-pin.good { background: var(--success-500); }
.map-pin.accent { background: var(--ldh-violet-500); }
.map-attrib {
  position: absolute; bottom: 6px; right: 8px;
  font-size: 10px; color: rgba(0,0,0,0.55); font-family: var(--font-sans);
  background: rgba(255,255,255,0.7); padding: 1px 6px; border-radius: 3px;
}
.map-scale {
  position: absolute; bottom: 28px; left: 14px;
  display: flex; align-items: center; gap: 6px;
  background: rgba(255,255,255,0.85); padding: 3px 8px; border-radius: 4px;
  font-family: var(--font-mono); font-size: 10px; color: var(--fg-2);
}
.map-scale .bar { display: inline-block; width: 50px; height: 4px; background: var(--ink-2); border-radius: 1px; }

/* Document edit modal — larger than search; reuses .ldh-prop-* primitives */
/* size="lg" + core header replace the old .ldh-doc-edit width/.mhead rules. */

/* IRI bar under the modal header */
.ldh-doc-iri-edit {
  padding: 10px var(--sp-6);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--ldh-blue-700);
  background: var(--surface-1);
  border-bottom: 1px solid var(--border-default);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Create-intent: parent container + slug input. The slug input grows to fill
   remaining space; the parent IRI is read-only and ellipses on overflow. */
.ldh-doc-iri-create {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px var(--sp-6);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  background: var(--surface-1);
  border-bottom: 1px solid var(--border-default);
  min-width: 0;
}
.ldh-doc-iri-create .msi {
  color: var(--ldh-blue-700);
  flex-shrink: 0;
}
.ldh-doc-iri-create .parent {
  color: var(--ldh-blue-700);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  flex-shrink: 1;
}
.ldh-doc-iri-create .trail {
  color: var(--ldh-blue-700);
  flex-shrink: 0;
}
.ldh-doc-iri-create .slug {
  font: inherit;
  color: var(--fg-1);
  background: var(--surface-0);
  border: 1px solid var(--border-default);
  border-radius: var(--r-sm);
  padding: 3px 8px;
  min-width: 0;
  flex: 1;
  outline: none;
  transition: border-color var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-fast) var(--ease-out-soft);
}
.ldh-doc-iri-create .slug:focus {
  border-color: var(--ldh-blue-400);
  box-shadow: 0 0 0 3px rgba(77, 148, 248, 0.18);
}
.ldh-doc-edit-body {
  flex: 1; overflow: auto;
  display: flex; flex-direction: column;
}

/* ---------- Constructor modal ---------- */
.ldh-ctor-modal { width: min(760px, calc(100vw - var(--sp-12))); max-height: calc(100vh - 96px); display: flex; flex-direction: column; }
.ldh-ctor-body { flex: 1; overflow: auto; padding: var(--sp-5) var(--sp-6); display: flex; flex-direction: column; gap: var(--sp-4); }
.ldh-ctor-lede { margin: 0; font-size: var(--fs-sm); color: var(--fg-2); line-height: var(--lh-base); }
.ldh-ctor-card {
  border: 1px solid var(--border-default); border-radius: var(--r-lg);
  background: var(--surface-1); overflow: hidden;
}
.ldh-ctor-card-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--border-default);
  background: var(--surface-0);
}
.ldh-ctor-card-head .ttl {
  font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: 0.02em;
  color: var(--ldh-blue-700); font-weight: var(--fw-semibold);
}
.ldh-ctor-rmctor {
  width: 28px; height: 28px; border: 0; background: transparent; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center; color: var(--fg-muted); cursor: pointer;
}
.ldh-ctor-rmctor:hover { background: var(--danger-50); color: var(--danger-500); }
.ldh-ctor-rows { display: flex; flex-direction: column; }
.ldh-ctor-empty { padding: var(--sp-4); font-size: var(--fs-sm); color: var(--fg-muted); text-align: center; }
.ldh-ctor-row {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) auto minmax(0, 1fr) 32px;
  align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  border-top: 1px solid var(--border-default);
}
.ldh-ctor-rows .ldh-ctor-row:first-child { border-top: 0; }
.ldh-ctor-row select {
  height: 36px; min-width: 0; padding: 0 var(--sp-2) 0 10px;
  border: 1px solid var(--border-default); border-radius: var(--r-sm);
  background: var(--surface-0); color: var(--fg-1);
  font: inherit; font-size: var(--fs-sm); cursor: pointer;
}
.ldh-ctor-row select.ctor-pred { font-weight: var(--fw-medium); }
.ldh-ctor-row select.ctor-range { font-family: var(--font-mono); }
.ldh-ctor-row select:focus-visible { outline: 0; border-color: var(--ldh-blue-400); box-shadow: 0 0 0 3px rgba(77,148,248,0.18); }

/* Predicate typeahead (combobox) */
.ctor-pred-ta { position: relative; min-width: 0; }
.ctor-pred-btn {
  display: flex; align-items: baseline; gap: 8px; width: 100%;
  height: 36px; padding: 0 8px 0 10px;
  border: 1px solid var(--border-default); border-radius: var(--r-sm);
  background: var(--surface-0); cursor: pointer; font: inherit; text-align: left;
}
.ctor-pred-btn:hover { border-color: var(--border-strong); }
.ctor-pred-btn.is-open { border-color: var(--ldh-blue-400); box-shadow: 0 0 0 3px rgba(77,148,248,0.18); }
.ctor-pred-btn .lbl { font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--fg-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ctor-pred-btn .curie { font-family: var(--font-mono); font-size: 10px; color: var(--fg-hint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; }
.ctor-pred-btn .caret { color: var(--fg-muted); align-self: center; flex-shrink: 0; }
.ctor-pred-pop {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 70;
  background: var(--surface-0); border: 1px solid var(--border-default);
  border-radius: var(--r-md); box-shadow: var(--shadow-md);
  overflow: hidden; min-width: min(320px, calc(100vw - 2 * var(--sp-4)));
  animation: menu-pop var(--dur-fast) var(--ease-out-soft);
}
.ctor-pred-search {
  display: flex; align-items: center; gap: 6px;
  padding: 8px 10px; border-bottom: 1px solid var(--border-default);
}
.ctor-pred-search .msi { color: var(--fg-muted); }
.ctor-pred-search input {
  flex: 1; border: 0; outline: none; background: transparent;
  font: inherit; font-size: var(--fs-sm); color: var(--fg-1);
}
.ctor-pred-list { max-height: 264px; overflow: auto; padding: 4px; }
.ctor-pred-opt {
  display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 8px;
  width: 100%; text-align: left;
  padding: 7px 8px; border: 0; background: transparent; border-radius: var(--r-sm); cursor: pointer;
}
.ctor-pred-opt:hover, .ctor-pred-opt.is-active { background: var(--surface-1); }
.ctor-pred-opt.is-sel { background: var(--bg-accent-quiet); }
.ctor-pred-opt .lbl { font-size: var(--fs-sm); color: var(--fg-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ctor-pred-opt .curie { font-family: var(--font-mono); font-size: 11px; color: var(--ldh-blue-700); }
.ctor-pred-opt .chk { color: var(--ldh-blue-700); }
.ctor-pred-empty { padding: 10px; display: flex; flex-direction: column; gap: 2px; font-size: var(--fs-sm); color: var(--fg-2); }
.ctor-pred-empty .hint { font-size: var(--fs-xs); color: var(--fg-muted); }
.ctor-term {
  display: inline-flex; padding: 2px; gap: 2px;
  background: var(--surface-2); border: 1px solid var(--border-default); border-radius: var(--r-md);
}
.ctor-term button {
  border: 0; background: transparent; cursor: pointer;
  padding: 4px 10px; border-radius: var(--r-sm);
  font: inherit; font-size: var(--fs-xs); font-weight: var(--fw-medium); color: var(--fg-2);
}
.ctor-term button:hover { color: var(--fg-1); }
.ctor-term button.is-on { background: var(--surface-0); color: var(--ldh-blue-700); box-shadow: var(--shadow-xs); }
.ctor-rm {
  width: 32px; height: 32px; border: 0; background: transparent; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center; color: var(--fg-muted); cursor: pointer;
}
.ctor-rm:hover { background: var(--danger-50); color: var(--danger-500); }
.ldh-ctor-addprop {
  display: flex; align-items: center; gap: 6px; width: 100%;
  padding: var(--sp-3) var(--sp-4);
  border: 0; border-top: 1px dashed var(--border-default);
  background: transparent; color: var(--ldh-blue-700);
  font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-medium); cursor: pointer;
}
.ldh-ctor-addprop:hover { background: var(--bg-accent-quiet); }
.ldh-ctor-addctor {
  align-self: flex-start;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 12px;
  border: 1px dashed var(--border-strong); border-radius: var(--r-md);
  background: transparent; color: var(--fg-2);
  font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-medium); cursor: pointer;
}
.ldh-ctor-addctor:hover { color: var(--fg-1); border-color: var(--ldh-blue-400); background: var(--surface-1); }

/* ============================================================
   Resource edit form — subject identity bar + form actions
   Shared by the read-mode property editor and the edit/create
   modal (via <ResourceEditForm>), so every authoring surface
   carries the identical subject-URI control and Actions menu.
   ============================================================ */
.ldh-edit-form { display: flex; flex-direction: column; }

/* Subject toggle is <IconButton pressed>; the primitive owns both states. */

/* Subject identity — a single row (term-type select + IRI field + copy +
   Actions), revealed by the header toggle. Replaces the live product's
   chevron-toggled URI row and the redundant read-only summary. */
.ldh-subject {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4) var(--sp-3) var(--sp-5);
  background: var(--surface-1);
  border-bottom: 1px solid var(--border-default);
}
.ldh-subject .term-select {
  flex-shrink: 0;
  height: 38px; padding: 0 var(--sp-2) 0 12px;
  border: 1px solid var(--border-default); border-radius: var(--r-sm);
  background: var(--surface-0); color: var(--fg-1);
  font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-medium);
  cursor: pointer;
}
.ldh-subject .term-select:focus-visible { outline: 0; border-color: var(--ldh-blue-400); box-shadow: 0 0 0 3px rgba(77,148,248,0.18); }
.ldh-subject .uri-field {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: var(--sp-2);
  background: var(--surface-0);
  border: 1px solid var(--border-default); border-radius: var(--r-sm);
  padding: 0 10px; height: 38px;
  transition: border-color var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-fast) var(--ease-out-soft);
}
.ldh-subject .uri-field .msi { color: var(--ldh-blue-700); flex-shrink: 0; }
.ldh-subject .uri-field .msi.warn { color: var(--danger-500); margin-left: auto; }
.ldh-subject .uri-field input {
  flex: 1; min-width: 0; border: 0; background: transparent; outline: none;
  font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--fg-1);
}
.ldh-subject .uri-field:focus-within { border-color: var(--ldh-blue-400); box-shadow: 0 0 0 3px rgba(77,148,248,0.18); }
.ldh-subject .uri-field.invalid { border-color: var(--danger-500); }
.ldh-subject .uri-field .parent { font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--ldh-blue-700); white-space: nowrap; flex-shrink: 1; overflow: hidden; text-overflow: ellipsis; }
.ldh-subject .uri-field .slug { flex: 1; }
.ldh-subject .uri-field .trail { font-family: var(--font-mono); color: var(--ldh-blue-700); flex-shrink: 0; }
.ldh-subject .bnode-inline {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: var(--sp-2);
  height: 38px; font-size: var(--fs-sm); color: var(--fg-2);
}
.ldh-subject code { font-family: var(--font-mono); background: var(--surface-0); border: 1px solid var(--border-default); padding: 2px 7px; border-radius: var(--r-xs); color: var(--fg-1); }
.ldh-subject-tools { display: flex; align-items: center; gap: var(--sp-1); flex-shrink: 0; }
.ldh-subject-copy {
  width: 34px; height: 34px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: transparent; border-radius: var(--r-sm);
  color: var(--fg-muted); cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft);
}
.ldh-subject-copy:hover { background: var(--bg-hover); color: var(--fg-1); }

/* Form action(s): a single action is a direct button; 2+ become a dropdown. */
.ldh-form-actions-wrap { position: relative; }
.ldh-form-action {
  display: inline-flex; align-items: center; gap: 6px;
  height: 34px; padding: 0 10px;
  border: 1px solid var(--border-default); border-radius: var(--r-sm);
  background: var(--surface-0); color: var(--fg-1);
  font: inherit; font-size: var(--fs-sm); font-weight: var(--fw-medium);
  cursor: pointer; white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out-soft), border-color var(--dur-fast) var(--ease-out-soft);
}
.ldh-form-action:hover { background: var(--bg-hover); border-color: var(--border-strong); }
.ldh-form-action .caret { margin-left: -2px; transition: transform var(--dur-fast) var(--ease-out-soft); }
.ldh-form-action.is-open .caret { transform: rotate(180deg); }
.ldh-form-actions-menu {
  position: absolute; right: 0; top: calc(100% + 6px);
  min-width: min(264px, calc(100vw - 2 * var(--sp-4)));
  background: var(--surface-0);
  border: 1px solid var(--border-default); border-radius: var(--r-md);
  box-shadow: var(--shadow-md);
  padding: var(--sp-1); z-index: 60;
  animation: menu-pop var(--dur-fast) var(--ease-out-soft);
}
.ldh-form-actions-menu .it {
  display: grid; grid-template-columns: 28px 1fr; gap: var(--sp-2);
  width: 100%; text-align: left;
  padding: var(--sp-2); border: 0; background: transparent;
  border-radius: var(--r-sm); cursor: pointer; font: inherit; color: var(--fg-1);
}
.ldh-form-actions-menu .it:hover { background: var(--surface-1); }
.ldh-form-actions-menu .it .ico {
  width: 28px; height: 28px; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg-accent-quiet); color: var(--ldh-blue-700);
}
.ldh-form-actions-menu .it .body { min-width: 0; display: flex; flex-direction: column; }
.ldh-form-actions-menu .it .lbl { font-size: var(--fs-sm); font-weight: var(--fw-medium); }
.ldh-form-actions-menu .it .sub { font-size: var(--fs-xs); color: var(--fg-2); }
.ldh-form-actions-menu .it.is-danger .ico { background: var(--danger-50); color: var(--danger-500); }

/* Modal chrome is core: .ac-backdrop / .ac-modal / .ac-modal-head|body|foot
   (ui_kits/core/overlays.css). The old .ldh-modal-backdrop + .ldh-modal + .mhead
   set was a SECOND modal system with no focus trap, no Escape handling and no
   depth stacking; all five dialogs now use core <Modal>, which has all three.
   What remains below is per-dialog BODY styling only. */
/* ---------- Access-request modal ---------- */
/* The dialog's leading glyph is core <Modal icon>; .ac-modal-icon styles it. */
.ar-body { padding: var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-4); overflow-y: auto; }
.ar-pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.ar-card {
  display: flex; flex-direction: column; gap: 4px;
  padding: var(--sp-3); border: 1px solid var(--border-default);
  border-radius: var(--r-md); background: var(--surface-1); min-width: 0;
}
.ar-k { font-size: var(--fs-xs); color: var(--fg-hint); font-weight: var(--fw-medium); }
.ar-v { display: flex; align-items: center; gap: 6px; font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--fg-1); }
.ar-v .msi { color: var(--fg-muted); }
.ar-iri {
  font-family: var(--font-mono); font-size: 11px; color: var(--ink-4);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ar-current {
  display: flex; align-items: center; gap: var(--sp-2);
  font-size: var(--fs-sm); color: var(--fg-2);
  background: var(--surf-sky); padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm);
}
.ar-current .msi { color: var(--bg-accent); flex-shrink: 0; }
.ar-modes { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.ar-modes legend { font-size: var(--fs-xs); color: var(--fg-hint); font-weight: var(--fw-medium); padding: 0; margin-bottom: 2px; }
.ar-mode {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3); border: 1px solid var(--border-default);
  border-radius: var(--r-md); cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out-soft), background var(--dur-fast) var(--ease-out-soft);
}
.ar-mode:hover { background: var(--bg-hover); }
.ar-mode input { position: absolute; opacity: 0; pointer-events: none; }
.ar-mode.is-on { border-color: var(--bg-accent); background: var(--bg-accent-quiet); }
.ar-mode-ic {
  width: 32px; height: 32px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-sm); background: var(--surface-2); color: var(--fg-muted);
}
.ar-mode.is-on .ar-mode-ic { background: var(--bg-accent); color: var(--fg-on-accent); }
.ar-mode-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.ar-mode-label { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--fg-1); }
.ar-mode-pred { font-family: var(--font-mono); font-size: 10px; font-weight: var(--fw-regular); color: var(--ink-4); }
.ar-mode-desc { font-size: var(--fs-xs); color: var(--fg-muted); }
.ar-mode-check { color: var(--bg-accent); opacity: 0; flex-shrink: 0; }
.ar-mode.is-on .ar-mode-check { opacity: 1; }
.ar-msg { display: flex; flex-direction: column; gap: 6px; }
.ar-opt { font-weight: var(--fw-regular); color: var(--ink-4); }
.ar-msg textarea {
  font: inherit; font-size: var(--fs-sm); color: var(--fg-1);
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border-default);
  border-radius: var(--r-md); background: var(--surface-0); resize: vertical;
}
.ar-msg textarea:focus { outline: none; border-color: var(--ldh-blue-400); box-shadow: 0 0 0 3px var(--bg-accent-quiet); }
.ar-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--border-default); background: var(--surface-1);
}
.ar-foot-note { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--fg-hint); }
.ar-foot-note .msi { font-size: 14px; }
.ar-foot-btns { display: flex; align-items: center; gap: var(--sp-2); }
.ar-sent { padding: var(--sp-8) var(--sp-6); text-align: center; display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); }
.ar-sent-ic {
  width: 56px; height: 56px; margin-bottom: var(--sp-2);
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 999px; background: var(--success-50); color: var(--success-500);
}
.ar-sent-ic .msi { font-size: 28px; }
.ar-sent h3 { margin: 0; font-size: var(--fs-lg); font-weight: var(--fw-semibold); color: var(--fg-1); }
.ar-sent p { margin: 0 0 var(--sp-3); font-size: var(--fs-sm); color: var(--fg-muted); max-width: 360px; }
/* Search field — the query input at the top of the search modal. Replaces
   .minput, which was search-only despite the generic name. */
.ldh-search-field {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 0 var(--sp-5);
  border-bottom: 1px solid var(--border-default);
  height: 60px; flex-shrink: 0;
}
.ldh-search-field > .msi { color: var(--fg-muted); flex-shrink: 0; }
.ldh-search-field input {
  flex: 1; min-width: 0; height: 100%; border: 0; outline: 0;
  background: transparent; font: inherit; font-size: var(--fs-lg); color: var(--fg-1);
}
.ldh-search-field input::placeholder { color: var(--fg-hint); }
/* type="search" makes WebKit/Blink render their OWN clear button, which sat
   next to our IconButton — two × controls, one unstyled and unthemed. The
   input keeps type="search" (correct semantics, and it gets the platform's
   search affordances) and we suppress the native decorations. */
.ldh-search-field input::-webkit-search-cancel-button,
.ldh-search-field input::-webkit-search-decoration,
.ldh-search-field input::-webkit-search-results-button,
.ldh-search-field input::-webkit-search-results-decoration {
  -webkit-appearance: none; appearance: none; display: none;
}

/* The embedded results view. Search results ARE a view — the real <View>
   block is mounted here, so it must sit flush inside the flush modal body
   rather than reading as a card floating in a dialog. */
.ldh-search-view { flex: 1; min-height: 0; overflow-y: auto; }
/* No chrome overrides here: the embedded <View> is passed `quiet`, so it
   renders .ldh-block.is-quiet — the variant that is already transparent and
   borderless, and which every skin already patches. Overriding .ldh-block from
   this scope instead would be a specificity war against each skin's own block
   rules (the m3 flow patch alone is (0,4,0)), and one the later-loaded skin
   file wins. */
/* The block row must not clip the parallax drawer's slide-in. */
.ldh-search-view .ldh-block-row { overflow: visible; }

.ac-modal .mtoolbar {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-5);
  background: var(--surface-1);
  border-bottom: 1px solid var(--border-default);
  font-size: var(--fs-sm); color: var(--fg-muted);
}
.ac-modal .mtoolbar .count b {
  display: inline-block; min-width: 20px; padding: 2px 6px;
  background: var(--ink-1); color: var(--surface-0); border-radius: var(--r-pill);
  font-family: var(--font-mono); font-size: 10px; font-weight: 600; text-align: center;
  margin-left: 6px;
}
.ac-modal .mtoolbar .right { margin-left: auto; display: flex; align-items: center; gap: var(--sp-2); }
.ac-modal .mlist { overflow-y: auto; flex: 1; padding: var(--sp-2); }
.ac-modal .mresult {
  display: grid; grid-template-columns: 32px 1fr auto;
  gap: var(--sp-3); align-items: center;
  padding: var(--sp-3); border-radius: var(--r-sm);
  cursor: pointer;
}
.ac-modal .mresult:hover { background: var(--surface-1); }
.ac-modal .mresult .ic {
  width: 32px; height: 32px; border-radius: var(--r-sm);
  background: var(--bg-accent-quiet); color: var(--ldh-blue-700);
  display: inline-flex; align-items: center; justify-content: center;
}
.ac-modal .mresult .ti { font-weight: var(--fw-medium); color: var(--ldh-blue-700); }
.ac-modal .mresult .meta { display: block; font-size: var(--fs-xs); color: var(--fg-muted); margin-top: 2px; font-family: var(--font-mono); }
.ac-modal .mresult .badge {
  font-family: var(--font-mono); font-size: 10px; color: var(--fg-muted);
  padding: 2px 8px; background: var(--surface-2); border-radius: var(--r-pill);
}
.ac-modal .mhint {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-5);
  background: var(--surface-1);
  border-top: 1px solid var(--border-default);
  font-size: var(--fs-xs); color: var(--fg-muted);
  /* Pinned: it is the last child of a flex-column flush body, so without this
     it would compress when the results region runs out of room. */
  flex-shrink: 0;
}
.ac-modal .mhint kbd {
  font-family: var(--font-mono); font-size: 10px;
  padding: 2px 6px; border: 1px solid var(--border-default); border-radius: 4px;
  background: var(--surface-0);
}
.chart-svg { width: 100%; height: 220px; }
.chart-svg .axis line, .chart-svg .axis path { stroke: var(--line-1); fill: none; }
.chart-svg .axis text { font-family: var(--font-mono); font-size: 9px; fill: var(--fg-muted); }
.chart-svg .grid line { stroke: var(--surface-3); stroke-dasharray: 2 3; }
.chart-svg .series-line { fill: none; stroke: var(--ldh-blue-500); stroke-width: 2; }
.chart-svg .series-area { fill: var(--ldh-blue-100); opacity: 0.5; }
.chart-svg .series-dot { fill: var(--ldh-blue-500); }
.chart-svg .bar { fill: var(--ldh-blue-500); }
.chart-svg .bar:hover { fill: var(--ldh-blue-600); }
.chart-svg .legend text { font-family: var(--font-sans); font-size: 10px; fill: var(--fg-muted); }

/* =========================================================================
   LEFT RAIL — a document tree, and nothing else.
   The search field, CLASSES and OTHER sections are gone (see DocumentTree.jsx
   for why each was duplication). What is left is the one thing no other surface
   does — lateral navigation through the container hierarchy — which is what
   makes persistent space defensible.

   ANATOMY: ul.ldh-tree > li.is-active > .tree-row > IconButton + a.tree-link
   Indentation comes from a --depth custom property rather than an inline
   paddingLeft per row: one rule owns the ramp, so changing it is one edit.
   ========================================================================= */
/* The rail is DATASPACE-specific, not global, so it starts below the header and
   the dataspace tab bar rather than at the top of the viewport — it belongs to
   the tab you are in, and spanning the chrome would claim otherwise.
   The offset is DERIVED from --ldh-chrome-h, never a literal: a hardcoded 96
   duplicates two heights declared elsewhere and misaligns silently if either
   changes. This only holds because the chrome is sticky (see .ldh-header). */
.ldh-sidebar {
  position: fixed; top: var(--ldh-chrome-h); left: 0; bottom: 0; z-index: 30;
  /* 264px is 68% of a 390px screen - the rail would cover the document it navigates. Capped the
     same way the kit caps .ac-drawer (surfaces.css) and .ldh-drawer, so every sliding panel in the
     product yields to the viewport by the same rule instead of three different ones. */
  width: min(var(--ldh-rail-w), 92vw); box-sizing: border-box;
  display: flex; flex-direction: column;
  background: var(--surface-1);
  border-right: 1px solid var(--border-default);
  transform: translateX(-100%);
  /* Fallback for engines without `inert` (the component sets that attribute
     when closed): `visibility: hidden` is the one hiding mechanism that DOES
     remove descendants from the tab order. Transitioned alongside the slide —
     visibility animates in discrete steps, so the panel stays visible for the
     whole slide-out and only then goes untabbable. */
  visibility: hidden;
  transition: transform var(--dur-base) var(--ease-out-soft),
              visibility var(--dur-base) var(--ease-out-soft);
}
.ldh-sidebar.is-open { transform: none; visibility: visible; }

.sb-head {
  display: flex; align-items: center; gap: var(--sp-2); flex-shrink: 0;
  padding: var(--sp-3);
  border-bottom: 1px solid var(--border-default);
}
/* Search is a BUTTON, not a readOnly input — it opens a dialog, so it must not
   impersonate a field. Styled like a field because that is the shape people
   look for, but the element tells the truth. */
.sb-search {
  display: flex; align-items: center; gap: var(--sp-2);
  flex: 1; min-width: 0; height: 32px; padding: 0 8px;
  background: var(--surface-0);
  border: 1px solid var(--border-default); border-radius: var(--r-md);
  cursor: pointer; text-align: left;
  font-family: var(--font-sans); font-size: var(--fs-sm); color: var(--fg-hint);
  transition: border-color var(--dur-fast) var(--ease-out-soft), background var(--dur-fast) var(--ease-out-soft);
}
.sb-search:hover { border-color: var(--border-strong); color: var(--fg-2); }
.sb-search:focus-visible { outline: 0; border-color: var(--ldh-blue-400); box-shadow: var(--focus-ring); }
.sb-search .msi { color: var(--fg-hint); flex-shrink: 0; }
.sb-search-label { flex: 1; min-width: 0; }
.sb-search kbd {
  flex-shrink: 0; padding: 1px 5px;
  background: var(--surface-2); border: 1px solid var(--border-default); border-radius: var(--r-xs);
  font-family: var(--font-mono); font-size: 10px; color: var(--fg-muted);
}

/* One scroll box for all sections, so the search row and the close button stay
   pinned while Documents + Classes scroll together. */
.sb-scroll { flex: 1; min-height: 0; overflow-y: auto; padding-bottom: var(--sp-4); }
.sb-section + .sb-section { border-top: 1px solid var(--border-default); margin-top: var(--sp-2); padding-top: var(--sp-2); }
.sb-heading {
  display: flex; align-items: center; gap: var(--sp-2);
  margin: 0; padding: var(--sp-2) var(--sp-3) var(--sp-1);
  font-family: var(--font-mono); font-size: var(--fs-xs); font-weight: var(--fw-medium);
  letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--fg-hint);
}
.sb-heading > span:first-child { flex: 1; min-width: 0; }

/* Classes — the TYPE axis. Same row vocabulary as the tree (.tree-link), since
   it is the same kind of statement; only the list wrapper differs. */
.sb-classes { list-style: none; margin: 0; padding: 0 var(--sp-2); }
/* Classes are a FLAT list — no hierarchy, so no disclosure column. They take a
   small dot marker rather than the tree's 28px spacer, which is what the
   reference does and why the spacer version read as misaligned: an indent that
   deep implies a parent the class list does not have. */
.sb-classes > li { display: flex; align-items: center; min-width: 0; border-radius: var(--r-sm); }
.sb-classes > li:hover { background: var(--bg-hover); }
.sb-classes > li.is-active { background: var(--bg-selected); box-shadow: inset 2px 0 0 var(--fg-selected); }
.sb-classes > li.is-active .tree-label { color: var(--fg-selected); font-weight: var(--fw-medium); }
/* The dot sits where the tree's disclosure chevron would, so the two lists share
   a left edge without the class rows borrowing an indent they have no use for. */
.sb-classes > li > .tree-link { padding-left: var(--sp-2); }
.sb-class-dot {
  width: 6px; height: 6px; flex-shrink: 0; margin-right: 2px;
  border-radius: 50%; background: var(--ldh-violet-400);
}
.sb-classes > li.is-active .sb-class-dot { background: var(--fg-selected); }

/* OTHER — Geo and Latest. A list sharing .tree-link, like the other two
   sections, so all three scan as one rail rather than three widgets. */
.sb-other { list-style: none; margin: 0; padding: 0 var(--sp-2); }
.sb-other > li { display: flex; min-width: 0; border-radius: var(--r-sm); }
.sb-other > li:hover { background: var(--bg-hover); }
.sb-other > li > .tree-link { padding-left: var(--sp-2); }
/* Trailing glyph marking an entry that opens a dialog rather than navigating. */
.sb-out { margin-left: auto; color: var(--ink-5); flex-shrink: 0; }

.sb-view {
  display: inline-flex; align-items: center; gap: 6px;
  flex: 1; justify-content: center; height: 30px; padding: 0 10px;
  background: var(--surface-0);
  border: 1px solid var(--border-default); border-radius: var(--r-md);
  color: var(--fg-2); text-decoration: none;
  font-family: var(--font-sans); font-size: var(--fs-sm); font-weight: var(--fw-medium);
  transition: background var(--dur-fast) var(--ease-out-soft), border-color var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft);
}
.sb-view:hover { background: var(--bg-accent-quiet); border-color: var(--ldh-blue-400); color: var(--ldh-blue-700); text-decoration: none; }
.sb-view:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
.sb-view .msi { color: var(--fg-hint); }
.sb-view:hover .msi { color: var(--ldh-blue-700); }

.ldh-tree, .ldh-tree ul { list-style: none; margin: 0; padding: 0; }
.ldh-tree { padding: 0 var(--sp-2); }
/* The row is the flex container; the LI carries state so the ground spans the
   twisty as well as the link (same reason as the tab strip's li.is-active). */
.ldh-tree .tree-row {
  display: flex; align-items: center; gap: 2px;
  padding-left: calc(var(--depth, 0) * 14px);
  border-radius: var(--r-sm);
}
.ldh-tree .tree-row:hover { background: var(--bg-hover); }
.ldh-tree > li.is-active > .tree-row,
.ldh-tree li.is-active > .tree-row {
  background: var(--bg-selected);
  box-shadow: inset 2px 0 0 var(--fg-selected);
}
.ldh-tree li.is-active > .tree-row .tree-label { color: var(--fg-selected); font-weight: var(--fw-medium); }
.ldh-tree li.is-active > .tree-row .tree-icon { color: var(--fg-selected); }

/* Shared row parts — UNSCOPED, because both navigation axes use them: the
   document tree and the class list are the same kind of row, so they must be
   the same rule. Scoping these under .ldh-tree is what left the class list's
   spacer at 123px (no width, so it just flexed) and the two axes 66px apart.
   Only genuinely tree-specific things stay scoped: the --depth ramp, the
   is-active ground, and the lazy-loading row.

   .tree-spacer matches the twisty's 28px box, so labels align whether or not a
   node has children. Inert — alignment, not a control. */
.tree-spacer { width: 28px; height: 28px; flex-shrink: 0; }

.tree-link {
  display: flex; align-items: center; gap: var(--sp-2);
  flex: 1; min-width: 0; padding: 5px var(--sp-2) 5px 0;
  color: var(--fg-2); text-decoration: none;
  font-family: var(--font-sans); font-size: var(--fs-sm);
  border-radius: var(--r-sm);
}
.tree-link:hover { color: var(--fg-1); text-decoration: none; }
.tree-link:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
.tree-icon { color: var(--fg-hint); flex-shrink: 0; }
.tree-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The member count is a core Tag; only its placement is scoped here. */
.tree-link > .ac-tag { flex-shrink: 0; }
/* Lazy children in flight. */
.ldh-tree .tree-loading {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 5px var(--sp-2) 5px calc(var(--depth, 0) * 14px + 28px);
  font-size: var(--fs-xs); color: var(--fg-hint);
}


/* ---------- Floating "Show sidebar" toggle ---------- */
/* Never visible at the same time as the rail: it is fixed bottom-left, and the
   rail spans from --ldh-chrome-h to the viewport bottom, so an open rail would
   have the pill sitting in its scroll area over two tree rows. The rail carries
   its own close button, so while it is open this control is redundant as well
   as in the way. index.html renders it conditionally; this is the guard for a
   consumer that does not. */
.ldh-sidebar.is-open ~ .sidebar-toggle { display: none; }
.sidebar-toggle {
  position: fixed; left: var(--sp-4); bottom: var(--sp-4);
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 12px;
  background: var(--ink-1);
  /* The ground is the ink token, so the text on it is the surface token — the two
     swap together and the chip stays legible in either scheme. */
  color: var(--surface-0);
  border: 0;
  border-radius: var(--r-pill);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.02em;
  cursor: pointer;
  box-shadow: var(--shadow-lg);
  z-index: 80;
  transition: transform var(--dur-fast) var(--ease-out-soft), background var(--dur-fast) var(--ease-out-soft);
}
.sidebar-toggle:hover { transform: translateY(-1px); background: var(--ldh-blue-700); }
.sidebar-toggle[aria-pressed="true"] { background: var(--ldh-blue-600); }
.sidebar-toggle .msi { color: color-mix(in srgb, var(--surface-0) 90%, transparent); }

/* Parallax navigation is <PivotBar> — its own row under the facet toolbar,
   one pill per predicate per direction. The old .ldh-parallax two-column rail
   is gone; pivots were undiscoverable in a side panel. */

/* =========================================================================
   Pager — container pagination (limit/offset), sits quietly under a view's
   list/table. Three zones: page-size selector · prev/status/next · page count.
   ========================================================================= */
.ldh-pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-4); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-6);
  margin-top: var(--sp-2);
  border-top: 1px solid var(--border-default);
}
/* When the pager sits flush under a grid/list body that already zeroes its
   side padding (inside .ldh-block-body), align it to the same edges. */
.ldh-block-body > .ldh-pager { padding-left: 0; padding-right: 0; }

/* A pager with nothing in it is not a pager. A client that re-renders its
   results asynchronously cannot emit the pager as part of that render — doing
   so destroys the control under the reader's cursor on every page flip, and
   removing and re-adding a bordered strip moves the page under them. So it
   renders the HOST once, beside the results region, and replaces only its
   CONTENTS; a single-page result set leaves it empty. Without this the empty
   host still draws its padding, margin and top border as a stray rule under
   the rows. Pair it with the :not(:empty) scoping below. */
.ldh-pager:empty { display: none; }

.ldh-pager-size { display: flex; align-items: center; gap: var(--sp-2); }
.ldh-pager-size label {
  font-family: var(--font-mono); font-size: var(--fs-xs);
  letter-spacing: var(--tracking-wide); text-transform: uppercase;
  color: var(--fg-muted);
}
.ldh-pager-select { position: relative; display: inline-flex; align-items: center; }
.ldh-pager-select select {
  appearance: none; -webkit-appearance: none;
  font-family: var(--font-sans); font-size: var(--fs-sm); color: var(--fg-1);
  background: var(--surface-0);
  border: 1px solid var(--border-default); border-radius: var(--r-sm);
  padding: 5px 26px 5px 10px; cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-fast) var(--ease-out-soft);
}
.ldh-pager-select select:hover { border-color: var(--border-strong); }
.ldh-pager-select select:focus-visible { outline: 0; border-color: var(--ldh-blue-400); box-shadow: var(--focus-ring); }
.ldh-pager-select .caret {
  position: absolute; right: 7px; pointer-events: none; color: var(--fg-hint);
}

.ldh-pager-nav { display: flex; align-items: center; gap: var(--sp-2); }
/* Pager prev/next are core <Button size="sm" appearance="outline">, which
   renders an <a> when given href, so paging stays URL-driven. */
.ldh-pager-status {
  display: inline-flex; align-items: baseline; gap: 5px;
  padding: 0 var(--sp-2);
  font-family: var(--font-mono); font-size: var(--fs-xs);
  color: var(--fg-muted); white-space: nowrap;
}
.ldh-pager-status b { color: var(--fg-1); font-weight: var(--fw-semibold); font-size: var(--fs-sm); }
.ldh-pager-status .of { color: var(--fg-hint); }

.ldh-pager-page {
  font-family: var(--font-mono); font-size: var(--fs-xs);
  color: var(--fg-hint); white-space: nowrap;
}
.ldh-pager-page b { color: var(--fg-2); font-weight: var(--fw-semibold); }

@media (max-width: 640px) {
  .ldh-pager { justify-content: center; }
  .ldh-pager-page { display: none; }
}

/* =========================================================================
   Nested blocks — block-in-block. Each level is a recessed well (no shadow);
   recession deepens and radius steps DOWN one stop per depth so deep nesting
   stays calm and corners never mix --r-xs with --r-2xl.
   ========================================================================= */
.ldh-nblock {
  border: 1px solid var(--border-default);
  background: var(--surface-2);
  border-radius: var(--r-md);
  /* Same constraint as .ldh-block above, and for the same reason: a nested block
     hosts a view's toolbar, and the mode switcher's .modes-pop is absolutely
     positioned inside it. `overflow: hidden` clips it to the well — a drop-up
     menu loses every item above the well's top edge — and the z-index the block
     takes while a menu is open cannot rescue it, since clipping happens before
     paint order is considered. Nothing paints into the corners to justify the
     clip: the head is transparent and the body is inset by its own padding. */
  overflow: visible;
  /* Its own query container, like .ldh-block: nesting is exactly where a component stops being as
     wide as the document and starts needing to know it. */
  container-type: inline-size;
  margin-top: var(--sp-4);
}
.ldh-text-block + .ldh-nblock { margin-top: var(--sp-5); }
/* Depth ramp: background recedes, radius steps down. */
.ldh-nblock[data-depth="1"] { background: var(--surface-2); border-radius: var(--r-md); }
.ldh-nblock[data-depth="2"] { background: var(--surface-3); border-radius: var(--r-sm); }
.ldh-nblock[data-depth="3"] { background: var(--surface-3); border-color: var(--border-strong); border-radius: var(--r-xs); }
/* Nested-in-nested spacing tightens slightly with depth. */
.ldh-nblock .ldh-nblock { margin-top: var(--sp-3); }

.ldh-nblock-head {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--border-default);
  background: transparent;
}
.ldh-nblock[data-depth="2"] .ldh-nblock-head,
.ldh-nblock[data-depth="3"] .ldh-nblock-head { border-bottom-color: var(--border-strong); }

.ldh-nblock-drag {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; flex-shrink: 0;
  color: var(--ink-4); cursor: grab; border-radius: var(--r-xs);
  transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft);
}
.ldh-nblock-drag:hover { background: var(--bg-hover); color: var(--fg-2); }
.ldh-nblock-drag:active { cursor: grabbing; }
.ldh-nblock-drag:focus-visible { outline: 0; box-shadow: var(--focus-ring); }

.ldh-nblock-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; flex-shrink: 0;
  border-radius: var(--r-sm);
  background: var(--surface-0); color: var(--fg-muted);
  border: 1px solid var(--border-default);
}
.ldh-nblock-titles { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
/* Title + type chips on one line, mirroring .ldh-res-titleline in the resource
   header. Replaces .ldh-nblock-kind, a 9.5px mono uppercase eyebrow that gave a
   chart or view a different type treatment from a person or a product. */
.ldh-nblock-titleline {
  display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; min-width: 0;
}
/* A nested header is smaller than a block header, so the shared chip steps down
   with it rather than dominating the title. */
.ldh-nblock-titleline .ldh-type-chip { height: 18px; padding: 0 6px 0 7px; font-size: 10px; }
.ldh-nblock-titleline .ldh-type-chip .msi { font-size: 11px; }
/* Kept live, not renamed: the shipped bundle still emits .ldh-nblock-kind until
   it recompiles, and pulling its styling in the same turn as the JSX change is
   what left the resource header unstyled earlier. Remove once no markup emits
   it. */
.ldh-nblock-kind {
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: var(--tracking-wide);
  text-transform: uppercase; color: var(--fg-hint);
}
.ldh-nblock-title {
  margin: 0; font-family: var(--font-sans); font-size: var(--fs-sm);
  font-weight: var(--fw-semibold); color: var(--fg-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ldh-nblock-actions { display: flex; align-items: center; gap: 2px; margin-left: auto; }
/* A nested toolbar just rests quieter and gains emphasis when its block is
   hovered; the box itself comes from <IconButton>. */
.ldh-nblock-actions .ac-iconbtn { color: var(--ink-4); }
.ldh-nblock:hover .ldh-nblock-actions .ac-iconbtn { color: var(--fg-muted); }

.ldh-nblock-body { padding: var(--sp-3); }
.ldh-nblock-lede { margin: 0; font-size: var(--fs-sm); color: var(--fg-muted); line-height: var(--lh-base); }
.ldh-nblock-foot {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-top: 1px dashed var(--border-default);
}
.ldh-nblock-note { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--fg-hint); }
/* A chart dropped into a nested block sits on white so the plot reads clearly. */
.ldh-nchart {
  background: var(--surface-0); border: 1px solid var(--border-default);
  border-radius: var(--r-sm); padding: var(--sp-3);
}

/* Tables are core <DataTable> (.ac-table). The four bespoke implementations
   here — .ldh-data-table, .ldh-mini-table, .ldh-map-table, .ldh-run-list —
   differed only in column tracks, which is now a --ac-cols variable. */

/* =========================================================================
   Admin surfaces — Ontology editor · Imports · SPARQL editor.
   All render inside .ldh-content and reuse the shell.
   ========================================================================= */
.ldh-admin { display: flex; flex-direction: column; gap: var(--sp-6); width: 100%; min-width: 0; }

/* Shared admin page header */
.ldh-admin-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4);
  flex-wrap: wrap;
}
.ldh-admin-head-main { display: flex; align-items: flex-start; gap: var(--sp-3); min-width: 0; }
.ldh-admin-head-icon {
  width: 44px; height: 44px; flex-shrink: 0; border-radius: var(--r-md);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg-accent-quiet); color: var(--ldh-blue-700);
}
.ldh-admin-head-icon .msi { font-size: 24px; }
.ldh-admin-eyebrow {
  font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: var(--tracking-wide);
  text-transform: uppercase; color: var(--ldh-blue-600);
}
.ldh-admin-title { margin: 2px 0 0; font-family: var(--font-sans); font-size: var(--fs-2xl); font-weight: var(--fw-semibold); letter-spacing: var(--tracking-snug); color: var(--fg-1); }
.ldh-admin-lede { margin: 6px 0 0; font-size: var(--fs-base); color: var(--fg-muted); line-height: var(--lh-base); max-width: 68ch; }
.ldh-admin-head-actions { display: flex; align-items: center; gap: var(--sp-2); flex-shrink: 0; }

/* Legacy segmented control. No JSX consumer remains — the import-type and
   per-browser pickers moved to core <Tabs>, and the view-mode picker to the
   <ModeSwitcher> dropdown. Kept for hand-written markup; prefer those. */
.ldh-seg {
  display: inline-flex; align-items: center; gap: 2px;
  padding: 3px; background: var(--surface-2);
  border: 1px solid var(--border-default); border-radius: var(--r-md);
  align-self: flex-start; flex-wrap: wrap;
}
.ldh-seg button {
  display: inline-flex; align-items: center; gap: 6px;
  height: 32px; padding: 0 14px; border: 0; background: transparent; cursor: pointer;
  font-family: var(--font-sans); font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--fg-muted);
  border-radius: var(--r-sm);
  transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft);
}
.ldh-seg button:hover { color: var(--fg-1); background: var(--bg-hover); }
.ldh-seg button.is-on { background: var(--surface-0); color: var(--ldh-blue-700); box-shadow: var(--shadow-xs); }
.ldh-seg button:focus-visible { outline: 0; box-shadow: var(--focus-ring); }

/* ---------- Ontology editor ---------- */
.ldh-onto-grid { display: grid; grid-template-columns: 300px 1fr; gap: var(--sp-5); align-items: start; }
.ldh-onto-list {
  display: flex; flex-direction: column; gap: var(--sp-4);
  background: var(--bg-card); border: 1px solid var(--border-default);
  border-radius: var(--r-lg); padding: var(--sp-4);
  /* Where the card parks is the COLUMN's to say, not the card's: a host with sticky chrome of its
     own has to clear it, and the kit cannot know how tall that is. The slot sets --ldh-aside-top;
     the kit's own value stays the fallback, so the ontology editor here is unchanged. */
  position: sticky; top: var(--ldh-aside-top, var(--sp-4));
}
.ldh-onto-group { display: flex; flex-direction: column; gap: 2px; }
.ldh-onto-group-h {
  font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: var(--tracking-wide);
  text-transform: uppercase; color: var(--fg-hint); padding: 0 var(--sp-2) var(--sp-1);
}
.ldh-onto-group-h em { font-style: normal; color: var(--ink-4); }
.ldh-onto-term {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2); border: 0; background: transparent; cursor: pointer; text-align: left;
  border-radius: var(--r-sm); width: 100%;
  transition: background var(--dur-fast) var(--ease-out-soft);
}
.ldh-onto-term:hover { background: var(--bg-hover); }
.ldh-onto-term.is-sel { background: var(--bg-selected); }
.ldh-onto-term:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
.ldh-onto-term-ic {
  width: 26px; height: 26px; flex-shrink: 0; border-radius: var(--r-sm);
  display: inline-flex; align-items: center; justify-content: center;
}
.ldh-onto-term-ic.class { background: var(--ldh-violet-100); color: var(--ldh-violet-700); }
.ldh-onto-term-ic.property { background: var(--success-50); color: var(--success-500); }
.ldh-onto-term-ic.lg { width: 44px; height: 44px; }
.ldh-onto-term-ic.lg .msi { font-size: 24px; }
.ldh-onto-term-body { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.ldh-onto-term-label { font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--fg-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ldh-onto-term.is-sel .ldh-onto-term-label { color: var(--ldh-blue-700); }
.ldh-onto-term-iri { font-family: var(--font-mono); font-size: 11px; color: var(--fg-hint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ldh-onto-term-count {
  margin-left: auto; font-family: var(--font-mono); font-size: 10px; color: var(--fg-muted);
  background: var(--surface-2); border-radius: var(--r-pill); padding: 1px 8px;
}

.ldh-onto-detail { background: var(--bg-card); border: 1px solid var(--border-default); border-radius: var(--r-lg); overflow: hidden; }
.ldh-onto-detail-head {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border-default);
}
.ldh-onto-detail-kind { font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--fg-hint); }
.ldh-onto-detail-title { margin: 2px 0; font-family: var(--font-sans); font-size: var(--fs-xl); font-weight: var(--fw-semibold); color: var(--fg-1); }
.ldh-onto-detail-iri { font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--ldh-blue-700); }
.ldh-onto-form { padding: var(--sp-2) var(--sp-5) var(--sp-5); }

@media (max-width: 900px) {
  .ldh-onto-grid { grid-template-columns: 1fr; }
  .ldh-onto-list { position: static; }
}

/* ---------- Imports ---------- */
.ldh-import-flow { display: flex; flex-direction: column; gap: var(--sp-5); }
.ldh-import-tabs > .ac-tabpanel { padding-top: var(--sp-5); }
.ldh-import-step {
  background: var(--bg-card); border: 1px solid var(--border-default);
  border-radius: var(--r-lg); padding: var(--sp-5);
}
/* Steps are core <Step> (.ac-step-*). */
.ldh-step-hint { font-size: var(--fs-sm); color: var(--fg-muted); }

.ldh-dropzone {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: var(--sp-8); text-align: center; cursor: pointer;
  border: 1.5px dashed var(--border-strong); border-radius: var(--r-md);
  background: var(--surface-1); color: var(--fg-muted);
  transition: border-color var(--dur-fast) var(--ease-out-soft), background var(--dur-fast) var(--ease-out-soft);
}
.ldh-dropzone .msi { font-size: 32px; color: var(--fg-hint); }
.ldh-dropzone:hover { border-color: var(--ldh-blue-400); background: var(--bg-accent-quiet); }
.ldh-dropzone:focus-visible { outline: 0; border-color: var(--ldh-blue-400); box-shadow: var(--focus-ring); }
.ldh-dropzone .dz-main { font-size: var(--fs-base); color: var(--fg-2); }
.ldh-dropzone .dz-main b { color: var(--fg-1); font-weight: var(--fw-semibold); }
.ldh-dropzone .dz-link { color: var(--fg-link); text-decoration: underline; }
.ldh-dropzone .dz-sub { font-size: var(--fs-xs); color: var(--fg-hint); font-family: var(--font-mono); }

.ldh-file-chip {
  display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-3);
  padding: var(--sp-2) var(--sp-3); background: var(--surface-2);
  border: 1px solid var(--border-default); border-radius: var(--r-sm);
}
.ldh-file-chip .fc-name { font-weight: var(--fw-medium); color: var(--fg-1); font-size: var(--fs-sm); }
.ldh-file-chip .fc-meta { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--fg-hint); }
.ldh-file-chip .fc-x {
  margin-left: auto; width: 26px; height: 26px; border: 0; background: transparent; cursor: pointer;
  color: var(--fg-muted); border-radius: var(--r-sm); display: inline-flex; align-items: center; justify-content: center;
}
.ldh-file-chip .fc-x:hover { background: var(--danger-50); color: var(--danger-500); }
.ldh-file-chip .fc-x:focus-visible { outline: 0; box-shadow: var(--focus-ring); }

/* Column-mapping cells. The table itself is core <DataTable>; these are the
   cell contents. */
.ldh-map-col { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; color: var(--fg-muted); }
.ldh-map-colname { font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--fg-1); font-weight: var(--fw-medium); }
.ldh-map-sample { font-size: var(--fs-xs); color: var(--fg-hint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ldh-map-arrow { display: inline-flex; justify-content: center; color: var(--ink-4); }
.ldh-map-pred { min-width: 0; }

.ldh-map-foot { display: flex; align-items: flex-end; gap: var(--sp-4); margin-top: var(--sp-4); flex-wrap: wrap; }
.ldh-inline-field { display: flex; flex-direction: column; gap: 5px; }
.ldh-inline-field > span { font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--fg-muted); }
.ldh-select-wrap { position: relative; display: inline-flex; align-items: center; }
.ldh-select-wrap select {
  appearance: none; -webkit-appearance: none;
  font-family: var(--font-sans); font-size: var(--fs-sm); color: var(--fg-1);
  background: var(--surface-0); border: 1px solid var(--border-default); border-radius: var(--r-sm);
  padding: 7px 28px 7px 10px; cursor: pointer; min-width: 180px;
  transition: border-color var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-fast) var(--ease-out-soft);
}
.ldh-select-wrap select:hover { border-color: var(--border-strong); }
.ldh-select-wrap select:focus-visible { outline: 0; border-color: var(--ldh-blue-400); box-shadow: var(--focus-ring); }
.ldh-select-wrap .caret { position: absolute; right: 8px; pointer-events: none; color: var(--fg-hint); }

/* Run-list cells; the table is core <DataTable>. */
.ldh-run-row > .msi { color: var(--fg-hint); }
.ldh-run-file { font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--fg-1); }
.ldh-run-rows { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--fg-muted); font-variant-numeric: tabular-nums; }
.ldh-run-note { color: var(--fg-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ldh-run-when { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--fg-hint); white-space: nowrap; text-align: right; }
.ldh-run-status { display: inline-flex; align-items: center; gap: 5px; padding: 2px 10px; border-radius: var(--r-pill); font-size: var(--fs-xs); font-weight: var(--fw-medium); white-space: nowrap; }
.ldh-run-status .msi { font-size: 15px; }
.ldh-run-status.completed { background: var(--success-50); color: var(--success-500); }
.ldh-run-status.running { background: var(--bg-accent-quiet); color: var(--ldh-blue-700); }
.ldh-run-status.running .msi { animation: spin 1.1s linear infinite; }
.ldh-run-status.queued { background: var(--surface-2); color: var(--fg-muted); }
.ldh-run-status.failed { background: var(--danger-50); color: var(--danger-500); }
@keyframes spin { to { transform: rotate(360deg); } }

/* URL / graph fields (RDF import) */
.ldh-url-field {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: 0 var(--sp-3); height: 44px;
  background: var(--surface-0); border: 1px solid var(--border-default); border-radius: var(--r-md);
  transition: border-color var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-fast) var(--ease-out-soft);
}
.ldh-url-field:focus-within { border-color: var(--ldh-blue-400); box-shadow: var(--focus-ring); }
.ldh-url-field > .msi { color: var(--fg-hint); }
.ldh-url-field input { flex: 1; min-width: 0; border: 0; background: transparent; outline: none; font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--fg-1); }
.ldh-url-field input::placeholder { color: var(--fg-hint); }
.ldh-url-fmt select, .ldh-url-field select {
  appearance: none; -webkit-appearance: none; border: 0; background: var(--surface-2);
  border-radius: var(--r-sm); padding: 4px 10px; font-size: var(--fs-xs); color: var(--fg-2); cursor: pointer;
}
.ldh-or { text-align: center; font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--fg-hint); text-transform: uppercase; letter-spacing: var(--tracking-wide); margin: var(--sp-3) 0; }
.ldh-check { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-3); font-size: var(--fs-sm); color: var(--fg-2); cursor: pointer; }
.ldh-check input { width: 16px; height: 16px; accent-color: var(--bg-accent); }
.ldh-import-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); margin-top: var(--sp-4); }

/* ---------- SPARQL editor ---------- */
.ldh-endpoint-note {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; border-radius: var(--r-pill);
  background: var(--surface-2); border: 1px solid var(--border-default);
  font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--fg-muted);
}
.ldh-endpoint-note .msi { color: var(--warning-500); }
.ldh-sparql-tabs { max-width: 100%; overflow-x: auto; }
.ldh-sparql-newtab { width: 30px; padding: 0 !important; justify-content: center; }

.ldh-sparql-pane {
  display: flex; background: light-dark(#0d1220, #0a0e17); border: 1px solid var(--border-default);
  border-radius: var(--r-md) var(--r-md) 0 0; overflow: hidden;
}
.ldh-sparql-gutter {
  display: flex; flex-direction: column; padding: var(--sp-4) var(--sp-3); text-align: right;
  font-family: var(--font-mono); font-size: var(--fs-sm); line-height: 1.65;
  color: rgba(255,255,255,0.28); background: rgba(255,255,255,0.03); user-select: none;
}
.ldh-sparql-code {
  flex: 1; margin: 0; padding: var(--sp-4); overflow-x: auto;
  font-family: var(--font-mono); font-size: var(--fs-sm); line-height: 1.65;
  color: rgba(255,255,255,0.86); tab-size: 2;
}
.ldh-sparql-code .t-kw  { color: #ff9ec4; font-weight: var(--fw-semibold); }
.ldh-sparql-code .t-fn  { color: #7cc7ff; }
.ldh-sparql-code .t-var { color: #ffd479; }
.ldh-sparql-code .t-str { color: #9be29b; }
.ldh-sparql-code .t-num { color: #f5b06b; }
.ldh-sparql-code .t-op, .ldh-sparql-code .t-pn { color: rgba(255,255,255,0.6); }

.ldh-sparql-bar {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3); background: var(--surface-2);
  border: 1px solid var(--border-default); border-top: 0; border-radius: 0 0 var(--r-md) var(--r-md);
  flex-wrap: wrap;
}
.ldh-sparql-meta { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--fg-muted); display: inline-flex; align-items: center; gap: 6px; }
.ldh-sparql-bar-actions { display: flex; gap: var(--sp-2); }

.ldh-sparql-results { background: var(--bg-card); border: 1px solid var(--border-default); border-radius: var(--r-lg); overflow: hidden; margin-top: var(--sp-4); }

/* Query block — a block backed by a SPARQL query. Results show by default; the
   editor pane folds out from the header's code toggle. No standalone editor page. */
.ldh-query-block .ldh-sparql { margin-bottom: var(--sp-4); }
.ldh-nblock .ldh-sparql { margin-bottom: var(--sp-3); }
.ldh-query-results { border: 1px solid var(--border-default); border-radius: var(--r-md); overflow: hidden; background: var(--surface-0); }
.ldh-query-results-h { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border-default); background: var(--surface-2); }
.ldh-query-results-h .rh-meta { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--fg-hint); }
/* The query toggle is <IconButton pressed>; aria-pressed drives its "on" look. */

/* =========================================================================
   Reusable form field + callout primitives (used by auth, could be used
   anywhere a labelled input or an inline status message is needed).
   ========================================================================= */
.ldh-field { display: flex; flex-direction: column; gap: 6px; }
.ldh-field-label { font-family: var(--font-sans); font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--fg-2); }
.ldh-field-input {
  display: flex; align-items: center; gap: var(--sp-2);
  height: 44px; padding: 0 var(--sp-3);
  background: var(--surface-0); border: 1px solid var(--border-default); border-radius: var(--r-md);
  transition: border-color var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-fast) var(--ease-out-soft);
}
.ldh-field-input > .msi { color: var(--fg-hint); }
.ldh-field-input:focus-within { border-color: var(--ldh-blue-400); box-shadow: var(--focus-ring); }
.ldh-field-input input { flex: 1; min-width: 0; border: 0; background: transparent; outline: none; font-family: var(--font-sans); font-size: var(--fs-base); color: var(--fg-1); }
.ldh-field-input input::placeholder { color: var(--fg-hint); }
.ldh-field-hint { font-size: var(--fs-xs); color: var(--fg-hint); }

.ldh-callout {
  display: flex; gap: var(--sp-3); padding: var(--sp-4);
  border-radius: var(--r-md); border: 1px solid var(--border-default); background: var(--surface-2);
}
.ldh-callout > .msi { flex-shrink: 0; }
.ldh-callout .co-body { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.ldh-callout .co-title { font-weight: var(--fw-semibold); color: var(--fg-1); font-size: var(--fs-sm); }
.ldh-callout .co-text { font-size: var(--fs-sm); color: var(--fg-2); line-height: var(--lh-base); }
.ldh-callout.is-warning { background: var(--warning-50); border-color: color-mix(in srgb, var(--warning-500) 32%, transparent); }
.ldh-callout.is-warning > .msi { color: var(--warning-500); }
.ldh-callout.is-danger { background: var(--danger-50); border-color: color-mix(in srgb, var(--danger-500) 32%, transparent); }
.ldh-callout.is-danger > .msi { color: var(--danger-500); }
.ldh-callout.is-info { background: var(--bg-accent-quiet); border-color: color-mix(in srgb, var(--ldh-blue-500) 28%, transparent); }
.ldh-callout.is-info > .msi { color: var(--ldh-blue-600); }

/* One-time password chip inside the callout */
.ldh-otp { display: flex; align-items: center; gap: var(--sp-2); margin-top: 4px; }
.ldh-otp code {
  flex: 1; font-family: var(--font-mono); font-size: var(--fs-base); letter-spacing: 0.08em;
  color: var(--fg-1); background: var(--surface-0); border: 1px solid var(--border-default);
  border-radius: var(--r-sm); padding: 8px 12px;
}
.ldh-otp-copy {
  display: inline-flex; align-items: center; gap: 5px; height: 38px; padding: 0 12px;
  border: 1px solid var(--border-default); background: var(--surface-0); cursor: pointer;
  border-radius: var(--r-sm); font-family: var(--font-sans); font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--fg-2);
  transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft);
}
.ldh-otp-copy:hover { background: var(--bg-hover); color: var(--fg-1); }
.ldh-otp-copy:focus-visible { outline: 0; box-shadow: var(--focus-ring); }

/* =========================================================================
   Auth screens — public sign-in / sign-up. Split: pastel hero + form card.
   ========================================================================= */
.ldh-auth { display: grid; grid-template-columns: 1.05fr 1fr; min-height: 100vh; background: var(--surface-1); }

.ldh-auth-hero {
  position: relative; overflow: hidden; color: var(--ink-1);
  background: var(--grad-aurora);
  display: flex; align-items: center;
  padding: var(--sp-16);
}
.ldh-auth-dots {
  position: absolute; inset: 0; pointer-events: none;
  background-image: radial-gradient(circle at 1px 1px, light-dark(rgba(11,14,20,0.10), rgba(255,255,255,0.08)) 1px, transparent 1.6px);
  background-size: 22px 22px;
  mask-image: radial-gradient(120% 100% at 100% 0%, #000 30%, transparent 75%);
  -webkit-mask-image: radial-gradient(120% 100% at 100% 0%, #000 30%, transparent 75%);
}
.ldh-auth-hero-inner { position: relative; z-index: 1; max-width: 440px; }
.ldh-auth-brand { display: inline-flex; align-items: center; gap: 10px; text-decoration: none; color: var(--ink-1); font-weight: var(--fw-semibold); font-size: var(--fs-md); margin-bottom: var(--sp-10); }
.ldh-auth-brand .mark { width: 26px; height: 26px; border-radius: 7px; background: var(--grad-deep); box-shadow: var(--shadow-sm); }
.ldh-auth-hero-title { font-family: var(--font-display); font-style: italic; font-size: var(--fs-5xl); line-height: var(--lh-tight); letter-spacing: var(--tracking-tight); color: var(--ink-1); margin: 0 0 var(--sp-4); }
.ldh-auth-hero-lede { font-size: var(--fs-md); line-height: var(--lh-base); color: var(--ink-2); margin: 0 0 var(--sp-8); }
.ldh-auth-points { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.ldh-auth-points li { display: flex; align-items: center; gap: var(--sp-3); font-size: var(--fs-base); color: var(--ink-2); }
.ldh-auth-points li .msi { color: var(--ldh-violet-600); background: color-mix(in srgb, var(--surface-0) 60%, transparent); border-radius: var(--r-sm); padding: 5px; }

.ldh-auth-main { display: flex; align-items: center; justify-content: center; padding: var(--sp-8); }
.ldh-auth-card { width: 100%; max-width: 420px; display: flex; flex-direction: column; gap: var(--sp-4); }
.ldh-auth-head { margin-bottom: var(--sp-2); }
.ldh-auth-head h2 { margin: 4px 0 0; font-family: var(--font-sans); font-size: var(--fs-2xl); font-weight: var(--fw-semibold); letter-spacing: var(--tracking-snug); color: var(--fg-1); }
.ldh-auth-eyebrow { font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--ldh-blue-600); display: inline-flex; align-items: center; gap: 5px; }
.ldh-auth-eyebrow.ok { color: var(--success-500); }

/* The auth screens' option buttons are core <ChoiceButton> — a two-line button
   on the shared button tokens. .ldh-auth-btn was a fourth button system. */
.ldh-auth-or { display: flex; align-items: center; gap: var(--sp-3); color: var(--fg-hint); font-family: var(--font-mono); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--tracking-wide); }
.ldh-auth-or::before, .ldh-auth-or::after { content: ""; flex: 1; height: 1px; background: var(--border-default); }
.ldh-auth-note { display: flex; align-items: flex-start; gap: 8px; font-size: var(--fs-sm); color: var(--fg-muted); line-height: var(--lh-base); margin: var(--sp-2) 0 0; }
.ldh-auth-note .msi { color: var(--fg-hint); margin-top: 1px; }
.ldh-auth-form { display: flex; flex-direction: column; gap: var(--sp-4); }
.ldh-auth-foot { text-align: center; font-size: var(--fs-sm); color: var(--fg-muted); margin: var(--sp-2) 0 0; }
.ldh-auth-foot a, .ldh-auth-note a { color: var(--fg-link); font-weight: var(--fw-medium); }

/* p12 result card */
.ldh-p12-card { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border: 1px solid var(--border-default); border-radius: var(--r-md); background: var(--surface-0); }
.ldh-p12-card .p12-ic { font-size: 32px; color: var(--ldh-violet-600); }
.ldh-p12-card .p12-body { display: flex; flex-direction: column; min-width: 0; }
.ldh-p12-card .p12-name { font-family: var(--font-mono); font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--fg-1); }
.ldh-p12-card .p12-meta { font-family: var(--font-mono); font-size: 11px; color: var(--fg-hint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ldh-p12-card .ac-btn { margin-left: auto; flex-shrink: 0; }

/* Per-browser install steps */
.ldh-install { display: flex; flex-direction: column; gap: var(--sp-3); }
/* The install tablist sits inline in the head row, so it must not stretch.
   The height override needs the sz-sm class too, or it loses on specificity to
   `.ac-tablist.sz-sm .ac-tab`. */
.ldh-install-tabs .ac-tablist { align-self: center; }
.ldh-install-tabs .ac-tablist.sz-sm .ac-tab { height: 28px; }
.ldh-install-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); flex-wrap: wrap; }
.ldh-install-steps { margin: 0; padding-left: 0; list-style: none; counter-reset: step; display: flex; flex-direction: column; gap: var(--sp-2); }
.ldh-install-steps li { position: relative; padding-left: 32px; font-size: var(--fs-sm); color: var(--fg-2); line-height: var(--lh-base); counter-increment: step; }
.ldh-install-steps li::before {
  content: counter(step); position: absolute; left: 0; top: -1px;
  width: 22px; height: 22px; border-radius: var(--r-pill);
  background: var(--bg-accent-quiet); color: var(--ldh-blue-700);
  font-family: var(--font-mono); font-size: 11px; font-weight: var(--fw-bold);
  display: inline-flex; align-items: center; justify-content: center;
}

@media (max-width: 860px) {
  .ldh-auth { grid-template-columns: 1fr; }
  .ldh-auth-hero { display: none; }
}


/* =========================================================================
   FORM MODE — the transitional whole-resource form.

   The end state is the per-row model above: click a value, edit in place,
   optimistic per-row saves. This variant keeps the SAME row anatomy but shows
   every control simultaneously and commits everything from one action bar.
   Both live side by side, chosen by `.is-form-mode` on .ldh-prop-form, so the
   port can move surface by surface and the per-row model is untouched.
   ========================================================================= */

/* Form mode looks EXACTLY like the resource-description block: values read as
   plain text on the same grid, with the same hover band and the same
   reveal-on-hover row actions. The only additions are the subject URI field +
   constructor button above, and the form action bar below.
   The difference from the per-row model is behavioural, not visual: every value
   is already an editable control, so there is no click-to-enter-edit step. The
   control's chrome therefore appears on hover and focus rather than on a
   committed "is-editing" state. Nothing here overrides the read row's
   geometry — that is the point. */
/* No per-row edit surface: the control carries its own (revealed) chrome, so
   the recessed .is-editing cell would be a box inside a box. */
.ldh-prop-form.is-form-mode .ldh-prop-row.is-editing .value {
  background: transparent; border-color: transparent; padding: 0;
  border-bottom-color: var(--border-default);
}

/* ---- Value stack: control, annotations, messages ----
   DECISION (C8): controls sit NAKED in the value cell — no per-control
   .ac-field-box wrapper. The core controls already carry field-box metrics
   (32/40/48), so wrapping would double the border and double the port's markup.
   The cell becomes a vertical stack instead, which is where the two lines the
   drawn row never had now live. */
.ldh-prop-row > .value > .val-stack,
.ldh-prop-row > .value.val-stack {
  display: flex; flex-direction: column; gap: 4px; min-width: 0; width: 100%;
  /* MUST be stretch: the base .value cell is a centered flex row (right for
     read mode), and a column that inherits align-items:center puts every
     control and tag in the middle of a 1fr column while any align-self:start
     child jumps to the left edge. */
  align-items: stretch;
}
/* Control + annotations on ONE line, so a form row is the same height as a
   read row. The control is capped rather than filling 1fr — a 1200px-wide
   text input next to a 60px label reads as a mistake, and the cap leaves the
   annotations sitting immediately after the value they describe. */
/* Control + annotations on ONE line, so a form row is the same height as a
   read row. Controls are capped rather than filling 1fr — a 1200px-wide text
   input next to a 60px label reads as a mistake, and the cap leaves the
   annotations sitting immediately after the value they describe.

   ONE width for every capped control, so their right edges align into a column.
   Per-control-type caps (text 280 / select 240 / combobox 340) were tried and
   rejected: sitting in the same form they read as ragged accidents, not as
   three intentional sizes. A control that genuinely needs the full column
   (code editor, upload target) opts out via `flex: 1 1 100%` below. */
/* Declared on :root, not on .ldh-prop-row. It is a real design decision — how
   wide a control may grow in a property row — so it registers as a token and a
   skin can retune it, rather than hiding as a local constant on a component. */
:root {
  --prop-control-w: 340px;
}
.ldh-prop-row .val-main {
  display: flex; align-items: center; gap: var(--sp-3); min-width: 0; flex-wrap: wrap;
}
.ldh-prop-row .val-main > .ac-field { flex: 0 1 var(--prop-control-w); max-width: var(--prop-control-w); }
.ldh-prop-row .val-main > .ac-select { flex: 0 1 var(--prop-control-w); max-width: var(--prop-control-w); }
.ldh-prop-row .val-main > .ac-combobox { flex: 0 1 var(--prop-control-w); max-width: var(--prop-control-w); }
.ldh-prop-row .val-main > .ldh-dt-pair { flex: 0 1 var(--prop-control-w); max-width: var(--prop-control-w); }
.ldh-prop-row .val-main > .ac-cb-committed { flex: 0 1 auto; }
.ldh-prop-row .val-main > .ldh-bool-seg { flex: 0 0 auto; }
/* Full-width controls: a code editor or an upload target needs the column. */
.ldh-prop-row .val-main > .ac-codefield,
.ldh-prop-row .val-main > .ac-fileinput { flex: 1 1 100%; }
/* basis 0 + shrink: the annot fills whatever space is left on the control's
   line and never pushes itself onto a second one. A wrapped annot line still
   occupies height even at opacity 0, which is exactly what made form rows
   taller than read rows. */
.ldh-annot { flex: 1 1 0; min-width: 0; overflow: hidden; }
/* NOTE: there is deliberately NO second "grow to fill" rule for these controls.
   An earlier revision added `flex: 1; min-width: 0` here for select / combobox /
   codefield / fileinput. For the first two it was dead (the caps above win), but
   for codefield and fileinput it silently REPLACED their `flex: 1 1 100%` with
   basis 0 — so the full-width controls the rule above promises were sized to
   content instead. One regime only: capped controls above, full-width controls
   at `1 1 100%`, and nothing re-declaring flex further down. */
/* TEXT-LIKE AT REST. Every control in form mode is a DS component now, so the
   at-rest treatment is applied to their boxes rather than to bare elements the
   app kit used to style itself: a form row is indistinguishable from a
   description row until you touch it. The border stays in the box model
   (transparent) so revealing it shifts nothing. */
.ldh-prop-form.is-form-mode .val-main > .ac-field .ac-field-box {
  background: transparent; border-color: transparent;
}
.ldh-prop-form.is-form-mode .ldh-prop-row:hover .val-main > .ac-field .ac-field-box {
  background: var(--surface-0); border-color: var(--border-default);
}
.ldh-prop-form.is-form-mode .val-main > .ac-field .ac-field-box:focus-within {
  background: var(--surface-0); border-color: var(--ldh-blue-400); box-shadow: var(--focus-ring);
}
/* A multiline value takes the column. */
.ldh-prop-row .val-main > .ac-field:has(textarea) { flex: 1 1 100%; max-width: none; }

/* Selects, comboboxes and the dateTime pair follow the same rule: text at rest,
   chrome on hover/focus. Otherwise a row with a select reads as a "form row"
   next to rows that read as description rows. */
.ldh-prop-form.is-form-mode .val-main > .ac-select > select,
.ldh-prop-form.is-form-mode .val-main > .ldh-dt-pair,
.ldh-prop-form.is-form-mode .val-main > .ac-combobox .ac-cb-box {
  background: transparent; border-color: transparent;
}
.ldh-prop-form.is-form-mode .ldh-prop-row:hover .val-main > .ac-select > select,
.ldh-prop-form.is-form-mode .ldh-prop-row:hover .val-main > .ldh-dt-pair,
.ldh-prop-form.is-form-mode .ldh-prop-row:hover .val-main > .ac-combobox .ac-cb-box {
  background: var(--surface-0); border-color: var(--border-default);
}
.ldh-prop-form.is-form-mode .val-main > .ac-select > select:focus-visible,
.ldh-prop-form.is-form-mode .val-main > .ldh-dt-pair:focus-within,
.ldh-prop-form.is-form-mode .val-main > .ac-combobox .ac-cb-box:focus-within {
  background: var(--surface-0); border-color: var(--ldh-blue-400); box-shadow: var(--focus-ring);
}
/* The select caret is chrome too — hidden until the row is live. */
.ldh-prop-form.is-form-mode .val-main > .ac-select .ac-select-caret { opacity: 0; }
.ldh-prop-form.is-form-mode .ldh-prop-row:hover .val-main > .ac-select .ac-select-caret,
.ldh-prop-form.is-form-mode .ldh-prop-row:focus-within .val-main > .ac-select .ac-select-caret { opacity: 1; }
/* A committed resource value reads as the read row's IRI link, not a pill,
   until the row is live. */
.ldh-prop-form.is-form-mode .val-main > .ac-cb-committed .ac-cb-chip {
  background: transparent; border-color: transparent; color: var(--ldh-blue-700);
}
.ldh-prop-form.is-form-mode .ldh-prop-row:hover .val-main > .ac-cb-committed .ac-cb-chip,
.ldh-prop-form.is-form-mode .ldh-prop-row:focus-within .val-main > .ac-cb-committed .ac-cb-chip {
  background: var(--bg-accent-quiet); color: var(--fg-selected);
  border-color: color-mix(in srgb, var(--fg-selected) 24%, transparent);
}
.ldh-prop-form.is-form-mode .val-main > .ac-cb-committed .cb-chip-btn { opacity: 0; }
.ldh-prop-form.is-form-mode .ldh-prop-row:hover .val-main .cb-chip-btn,
.ldh-prop-form.is-form-mode .ldh-prop-row:focus-within .val-main .cb-chip-btn { opacity: 1; }
/* Annotations are editing metadata, not description content: inline (so they
   never add height) and revealed with the rest of the row's chrome — with ONE
   exception, and the fade moves off the strip onto its children to carve it out.
   A LANGUAGE IS NOT METADATA ABOUT THE VALUE, IT IS HALF OF THE VALUE: "Denmark"@en
   and "Denmark"@da are two different literals, and a concept carrying five
   skos:prefLabels is told apart by nothing else. Hidden, the form showed five
   identical-looking rows, "one prefLabel per language" could not be checked by eye,
   and switching from a read view that shows the tag at rest made it disappear.
   The datatype chip stays chrome: rdf:langString follows from having a language at
   all, and xsd:dateTime repeats what the date control already shows.
   The strip keeps its own layout rules either way (nowrap, basis 0), so nothing
   about the earlier height finding changes — only which boxes are painted.
   The selector is exactly "a language exists". xhtml:Input omits @value when the
   tag is empty, so `input[value]` alone would do for the product - the
   `:not([value=""])` is for the kit, where React reflects a controlled input's
   value to the attribute and writes an empty one as value="". Both ends agree
   under the pair, and a blank field stays chrome and reveals with the rest. */
.ldh-prop-form.is-form-mode .ldh-annot > * {
  opacity: 0; transition: opacity var(--dur-fast) var(--ease-out-soft);
}
.ldh-prop-form.is-form-mode .ldh-prop-row:hover .ldh-annot > *,
.ldh-prop-form.is-form-mode .ldh-prop-row:focus-within .ldh-annot > *,
.ldh-prop-form.is-form-mode .ldh-annot > .ldh-lang:has(input[value]:not([value=""])) { opacity: 1; }

/* IRI-typed values are machine-readable, so they type in mono. Resource values
   render as <Combobox iri>, which already sets this on its own input. */


/* ---- Annotations line (C10): ONE system for term kind, datatype, language ----
   The read side used pills and the edit side grey micro-text; both now use
   .ac-tag sz-sm em-quiet, so a datatype reads identically in a read row and
   an edit row. Term kind is encoded by colour via the RDF hues. */
/* NOWRAP is load-bearing. With flex-basis 0 (set below so the annot never
   pushes itself onto its own line) the tags wrap against a 0-width hypothetical
   box BEFORE the item grows, so a wrapping annot silently became 3-4 line boxes
   tall and inflated every form row to twice a read row. Clipping revealed
   metadata is the right trade. */
.ldh-annot { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap; min-width: 0; }
.ldh-annot > .ac-tag { flex-shrink: 0; }
.ldh-annot .an-term.is-literal  { background: var(--success-50); color: var(--rdf-literal); }
.ldh-annot .an-term.is-resource { background: var(--bg-accent-quiet); color: var(--rdf-resource); }
.ldh-annot .an-term.is-class    { background: var(--ldh-violet-100); color: var(--rdf-class); }
.ldh-annot .an-term.is-blank    { background: var(--warning-50); color: var(--rdf-blank); }
/* Language tag. READ side: the value's @xml:lang renders as the core Tag in its
   quiet violet at the new xs size — <span class="ac-tag em-quiet co-accent
   sz-xs">. It was an LDH-only .chip-inline with the same violet-50/violet-700
   tones, i.e. a Tag that had not been told it was one. Violet is the system's
   "structural/semantic annotation" hue, which is exactly what a language tag is.
   EDIT side: a narrow TextField (below) — it used to be a bespoke pill with its
   own bordered input; the only thing it needed was a width. */
.ldh-annot > .ac-tag.sz-xs { flex-shrink: 0; }
.ldh-lang { display: inline-flex; min-width: 0; }
/* The strip clips (overflow hidden) and its chips do not shrink, so without this
   the language INPUT was the first thing to lose width in a short strip. */
.ldh-annot > .ldh-lang { flex-shrink: 0; }
.ldh-lang > .ac-field { width: 11ch; }
/* Pill in BOTH states. The field is a language tag whichever side of the read/edit
   line it is on, so the radius is not something hover hands it — that leaves fill
   and ink as the only difference between the two, and nothing to transition that
   .ac-field-box does not already transition. */
.ldh-lang > .ac-field .ac-field-box { min-height: 24px; padding: 0 6px; border-radius: var(--r-pill); }
.ldh-lang > .ac-field .ac-field-box input {
  font-family: var(--font-mono); font-size: var(--fs-xs); padding: 2px 0;
}
/* At rest a form-mode language field wears the READ side's tag: the same
   .ac-tag.em-quiet.co-accent tokens ac:lang-tag renders, so a row being edited still
   looks like the row that describes it — which is the whole at-rest regime above,
   applied to the one annotation that stays visible. Mono, not the Tag's sans: a
   language subtag is machine-readable and the field already types it that way, and a
   family swap on hover would reflow the box. Fill, border and ink only — the box
   keeps its height and its 11ch width, so revealing the chrome moves nothing. */
.ldh-prop-form.is-form-mode .ldh-lang > .ac-field .ac-field-box {
  background: var(--ldh-violet-100); border-color: transparent;
}
.ldh-prop-form.is-form-mode .ldh-lang > .ac-field .ac-field-box input,
.ldh-prop-form.is-form-mode .ldh-lang > .ac-field .ac-field-box .ac-adorn {
  color: var(--ldh-violet-700);
}
.ldh-prop-form.is-form-mode .ldh-prop-row:hover .ldh-lang > .ac-field .ac-field-box,
.ldh-prop-form.is-form-mode .ldh-lang > .ac-field .ac-field-box:focus-within {
  background: var(--surface-0); border-color: var(--border-default);
}
.ldh-prop-form.is-form-mode .ldh-prop-row:hover .ldh-lang > .ac-field .ac-field-box input,
.ldh-prop-form.is-form-mode .ldh-lang > .ac-field .ac-field-box:focus-within input { color: var(--fg-1); }
.ldh-prop-form.is-form-mode .ldh-prop-row:hover .ldh-lang > .ac-field .ac-field-box .ac-adorn,
.ldh-prop-form.is-form-mode .ldh-lang > .ac-field .ac-field-box:focus-within .ac-adorn { color: var(--fg-hint); }

/* dateTime — two TextFields (value + offset) sitting together. They were a
   single bespoke bordered shell with a divider; that shell was the app kit
   re-implementing a field box, so it is gone. The offset field is narrow and
   does not shrink; the datetime field absorbs the row's control cap. */
.ldh-dt-pair { display: inline-flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.ldh-dt-pair > .ac-field:first-child { flex: 1 1 auto; min-width: 0; }
.ldh-dt-pair > .ac-field:last-child { flex: 0 0 9ch; }
.ldh-dt-pair > .ac-field .ac-field-box input {
  font-family: var(--font-mono); font-size: var(--fs-xs);
}

/* ---- Boolean values (C11) ----
   DECISION: a 3-way segmented control, NOT .ac-toggle. A toggle means "takes
   effect immediately", which is wrong inside an explicit Save/Cancel form, and
   a two-state toggle cannot express an RDF boolean that is simply absent.
   true / false / unset are all reachable in one click. */
.ldh-bool-seg { display: inline-flex; align-self: flex-start; }
.ldh-bool-seg .ac-switch-seg[aria-checked="true"] { color: var(--fg-selected); }
.ldh-bool-seg .seg-unset[aria-checked="true"] { color: var(--fg-muted); }

/* ---- Violation + help messages (C9) ----
   Reaches EVERY control kind, not just input/textarea: the invalid decoration
   is driven from the row, so a select, combobox chip, file input and code field
   all pick it up. */
.ldh-vmsgs { display: flex; flex-direction: column; gap: 3px; }
.ldh-prop-group.is-violation > .label .pred { color: var(--danger-500); }
.ldh-prop-group.is-violation > .label { box-shadow: inset 2px 0 0 var(--danger-500); }
.ldh-prop-row.is-violation .val-main > .ac-field .ac-field-box,
.ldh-prop-row.is-violation .val-main > .ac-select > select,
.ldh-prop-row.is-violation .val-main > .ac-combobox .ac-cb-box,
.ldh-prop-row.is-violation .val-main > .ac-codefield,
.ldh-prop-row.is-violation .val-main > .ldh-dt-pair .ac-field-box { border-color: var(--danger-500); }
.ldh-prop-row.is-violation .val-main > .ac-fileinput .ac-file-drop { border-color: var(--danger-500); background: var(--danger-50); }
.ldh-prop-row.is-violation .val-main > .ac-cb-committed .ac-cb-chip { border-color: var(--danger-500); color: var(--danger-500); background: var(--danger-50); }

/* ---- Label slots: required marker + predicate description (C8) ----
   DECISION (rdfs:comment): a TOGGLETIP on the label, not a hint line under it.
   The label column is a fixed 200px and right-aligned; a wrapped sentence in it
   would push every value row apart and destroy the scan-down rhythm the read
   view depends on. The description is reference material, not guidance you need
   before typing — click-to-read is the right cost, and a toggletip works on
   touch where a tooltip does not. */
.ldh-prop-group > .label .lbl-row { display: inline-flex; align-items: center; gap: 4px; justify-content: flex-end; flex-wrap: wrap; }
.ldh-prop-group > .label .ac-label-aux {
  margin-left: 0; font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--fg-hint);
}
/* Required marker — .ac-label-aux is its home, replacing the bridge's
   ::after ' *'. It carries a text label for assistive tech, not just a glyph. */
.ldh-prop-group > .label .req { color: var(--danger-500); font-weight: var(--fw-bold); }
.ldh-prop-group > .label .ac-toggletip-btn { width: 18px; height: 18px; }
.ldh-prop-group > .label .ac-toggletip-btn .msi { font-size: 14px; }

/* ---- Per-property add-value affordance ----
   It is the small + in .row-actions (an <IconButton size="xs">), matching the
   per-row model exactly. A labelled dashed strip under each group was tried and
   rejected: it added a row of height to every predicate, which pushed the form
   off the read view's layout entirely. */

/* ---- Arrival treatment (C12): a just-added row announces itself once ---- */
@keyframes ldh-row-arrive {
  from { background: var(--bg-accent-quiet); }
  to   { background: transparent; }
}
.ldh-prop-row.is-arrived > .value,
.ldh-prop-row.is-arrived > .row-actions { animation: ldh-row-arrive 1.1s var(--ease-out-soft); }
@media (prefers-reduced-motion: reduce) {
  .ldh-prop-row.is-arrived > .value,
  .ldh-prop-row.is-arrived > .row-actions { animation: none; }
}

/* =========================================================================
   FORM ACTION BAR (A) — one button vocabulary.
   Spec'd in the .ac-btn matrix — the one button system in the design system.
   Save is gated on dirty. 2-button (Save/Cancel) and 3-button (+ Reset) forms
   are the same bar with a conditional child.
   ========================================================================= */
.ldh-form-bar {
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-6);
  border-top: 1px solid var(--border-default);
  background: var(--surface-1);
}
/* Inline placement (a form in a block) keeps the card's bottom radius; the
   modal-footer placement is .ac-modal-foot and needs neither. */
.ldh-form-bar.pl-inline { border-radius: 0 0 var(--r-lg) var(--r-lg); }
/* Dialog placement. A dialog hangs its actions outside the scrolling body as
   .ac-modal-foot, but a form's actions cannot go there: the form element has to wrap
   the fields and the buttons together, so the bar rides at the end of the body and
   scrolled away with the fields - a long form put Save behind a scroll. Sticking it to
   the scrollport's bottom edge lands it where .ac-modal-foot sits, in reach from the
   first field on. */
.ac-modal-body .ldh-form-bar { position: sticky; bottom: 0; z-index: 1; border-radius: 0 0 var(--r-lg) var(--r-lg); }
/* Nothing may sit below a stuck bar, or the rows scrolling under it stay visible through
   the strip left between it and the dialog's edge: the body's own bottom inset goes, and
   so does every wrapper's between the two. Selected with :has() rather than named,
   because the kit cannot know what a consumer nests between its dialog and its form -
   and guarded on the bar closing the body, because what a dialog parks after its form
   (the add-data notice) is the dialog's content, not the form's: it keeps the inset and
   scrolls out from under the bar at the form's end. */
.ac-modal-body:has(> :last-child .ldh-form-bar:last-child), .ac-modal-body:has(> :last-child .ldh-form-bar:last-child) *:has(.ldh-form-bar) { padding-bottom: 0; }
/* A form that IS the dialog's body content carries no card frame of its own, so its bar
   is the dialog's foot and spans the body's side inset. A form nested in a block keeps
   that card's frame and stays within it, and a bar with dialog content after it is the
   form's footer rather than the dialog's - same guard as the inset above. */
.ac-modal-body:has(> :last-child .ldh-form-bar:last-child):not(.is-flush) > form > .ldh-form-bar { margin-inline: calc(-1 * var(--sp-5)); }
.ldh-form-bar .fb-end { margin-left: auto; display: flex; align-items: center; gap: var(--sp-2); }
/* Leading cluster: actions on the form as a whole, opposite the submit cluster. It needs no margin of
   its own — .fb-end's margin-left: auto does the separating. */
.ldh-form-bar .fb-start { display: flex; align-items: center; gap: var(--sp-2); }
/* The standalone .ldh-form-action chrome is 34px; in the bar it sits beside .ac-btn.sz-sm buttons. */
.ldh-form-bar .ldh-form-action { height: 30px; }
/* A footer menu drops out of the card, so it opens upward, and it anchors to the trigger's left edge
   because the cluster sits at the start of the bar rather than the end. */
.ldh-form-bar .ldh-form-actions-menu { top: auto; bottom: calc(100% + 6px); left: 0; right: auto; }
/* Dirty-state readout — the form says what will happen, so Save being disabled
   is never a mystery. */
.ldh-form-status {
  display: inline-flex; align-items: center; gap: 5px;
  font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--fg-hint);
}
.ldh-form-status .msi { font-size: 14px; }
.ldh-form-status.is-dirty { color: var(--warning-500); }
.ldh-form-status.is-saving { color: var(--fg-muted); }
.ldh-form-status.is-saved { color: var(--success-500); }
/* Submitting: the whole field area goes inert and loses pointer events, so a
   second Save can't race the first. Opacity stays high — this is a lock, not a
   disabled form, and the values must remain readable while it commits. */
.ldh-prop-form.is-submitting { pointer-events: none; opacity: 0.72; }
.ldh-prop-form.is-submitting * { cursor: progress !important; }

/* Conflict alert (A) — .ac-alert at the top of the form, replacing the legacy
   .alert.alert-error. Sits inside the form's own scroll area so it travels with
   the fields it describes. */
.ldh-form-alert { padding: var(--sp-4) var(--sp-6) 0; }
.ldh-form-alert:empty { display: none; }

/* =========================================================================
   FIELDSET / SECTION (C14) — two legend styles, both intentional.
     .lg-eyebrow  10px mono uppercase, inline in the flow — for grouping rows
                  inside an already-titled surface (a block, a modal body).
                  It is a divider with a name, not a heading.
     .lg-heading  18px semibold — for a section that is its own subject, e.g.
                  a blank-node child whose statements are about a new resource.
   Nesting: each level indents by --sp-4 and takes a left rule, capped at depth
   3 for the same reason nested blocks are — beyond that, indentation stops
   carrying meaning and the label column has nowhere left to go.
   ========================================================================= */
.ldh-fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
.ldh-fieldset > legend { padding: 0; }
.ldh-fieldset > legend.lg-eyebrow {
  display: block; width: 100%; box-sizing: border-box;
  padding: var(--sp-4) var(--sp-6) var(--sp-2);
  font-family: var(--font-mono); font-size: var(--fs-xs); font-weight: var(--fw-medium);
  letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--fg-hint);
}
.ldh-fieldset > legend.lg-heading {
  padding: var(--sp-4) var(--sp-6) var(--sp-1);
  font-family: var(--font-sans); font-size: var(--fs-lg); font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-snug); color: var(--fg-1);
}
.ldh-fieldset > .fs-desc { padding: 0 var(--sp-6) var(--sp-2); font-size: var(--fs-sm); color: var(--fg-muted); }
/* Legendless variant — a group that needs the nesting frame but no name. */
.ldh-fieldset.is-bare > legend { display: none; }
.ldh-fieldset[data-depth="1"], .ldh-fieldset[data-depth="2"], .ldh-fieldset[data-depth="3"] {
  margin-left: var(--sp-4); border-left: 1px solid var(--border-default);
}
.ldh-fieldset[data-depth="2"] { border-left-color: var(--border-strong); }
.ldh-fieldset[data-depth="3"] { border-left-style: dashed; }

/* =========================================================================
   PERMISSION-GATED ACTIONS (C17)
   DECISION: three distinct treatments, never display:none-until-known.
     · pending  → rendered DISABLED with a "checking permissions" title, so the
                  layout is final before the answer arrives and nothing shifts.
     · denied   → ABSENT. An action the user can never take is not information;
                  a permanently disabled button is just a dead end they keep
                  returning to.
     · allowed  → rendered normally.
   The inline display:none the live product uses is the one option ruled out —
   it reflows the toolbar the moment permissions resolve.
   ========================================================================= */
.ldh-perm-pending { opacity: 0.5; pointer-events: none; }


/* The subject-URI toggle. A single quiet control on its own line, right-aligned
   over the statements' actions column so it reads as belonging to the form's
   chrome rather than to any one statement. */
.ldh-form-subjbar {
  display: flex; justify-content: flex-end;
  padding: var(--sp-2) var(--sp-6) 0 0;
}

/* =========================================================================
   SUBJECT ROW IN FORM MODE
   The standalone .ldh-subject bar is a chunky bordered strip — it reads as a
   legacy form header bolted above a description block. In form mode it keeps
   the SAME grid as the property rows: term type in the label column, the URI
   field in the value column, tools in the 64px action column, so the columns
   line up down the whole fieldset.
   Alignment is not sameness, though. The row states the resource's IDENTITY
   rather than one of its properties: there is exactly one of it, it is the
   head of the description, and editing it re-addresses the resource. So it
   takes three quiet marks of its own — a heavier closing rule than the
   separators between property rows, a term-type control that reads as an
   identity label at rest, and tools that stay visible instead of fading with
   a hover. The fade is there to stop N property rows repeating the same
   chrome N times; a singleton row has no repetition to suppress.
   ========================================================================= */
.ldh-edit-form .ldh-subject {
  display: grid;
  grid-template-columns: 200px minmax(0, 1fr) 64px;
  column-gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-2) 0;
  background: transparent;
  border-bottom: 1px solid var(--border-default);
}
/* The row is marked as the head of the description, not detached from it, so it collapses on the same
   stop and in the same shape as .ldh-prop-group: term type above, URI field and tools below. Diverging
   here would put the subject row back to being a header bolted on top, which is what this replaced. */
@container (max-width: 520px) {
  .ldh-edit-form .ldh-subject { grid-template-columns: minmax(0, 1fr) auto; }
  .ldh-edit-form .ldh-subject > .term-select { grid-column: 1 / -1; justify-self: start; }
  .ldh-edit-form .ldh-subject-tools { padding-right: var(--sp-4); }
}
.ldh-edit-form .ldh-subject-tools { padding-right: var(--sp-6); }
/* The term-type select acts as this row's label, so it aligns like one — and at rest it reads as one:
   the identity caption of the row, in the same muted mono the fieldset legend uses, rather than a
   control competing with the property labels. It picks up select chrome on hover and focus exactly as
   the URI field beside it does, so nothing is lost. */
.ldh-edit-form .ldh-subject .term-select {
  justify-self: end;
  height: 28px; padding: 0 6px 0 8px;
  background: transparent; border-color: transparent;
  font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--fg-muted);
  transition: background var(--dur-fast) var(--ease-out-soft), border-color var(--dur-fast) var(--ease-out-soft);
}
.ldh-edit-form .ldh-subject:hover .term-select,
.ldh-edit-form .ldh-subject:focus-within .term-select {
  font-family: inherit; font-size: var(--fs-sm); letter-spacing: normal; text-transform: none;
  color: var(--fg-2);
}
.ldh-edit-form .ldh-subject:hover .term-select { background: var(--surface-0); border-color: var(--border-default); }
.ldh-edit-form .ldh-subject .term-select:focus-visible { background: var(--surface-0); border-color: var(--ldh-blue-400); box-shadow: var(--focus-ring); }
/* URI field: mono text at rest, exactly like the read row's IRI, with chrome on
   hover/focus like every other control in form mode. */
.ldh-edit-form .ldh-subject .uri-field,
.ldh-edit-form .ldh-subject .bnode-inline {
  height: 28px; padding: 0 6px;
  background: transparent; border-color: transparent;
}
.ldh-edit-form .ldh-subject:hover .uri-field { background: var(--surface-0); border-color: var(--border-default); }
.ldh-edit-form .ldh-subject .uri-field:focus-within {
  background: var(--surface-0); border-color: var(--ldh-blue-400); box-shadow: var(--focus-ring);
}
.ldh-edit-form .ldh-subject .uri-field > .msi { opacity: 0; transition: opacity var(--dur-fast) var(--ease-out-soft); }
.ldh-edit-form .ldh-subject:hover .uri-field > .msi,
.ldh-edit-form .ldh-subject:focus-within .uri-field > .msi { opacity: 1; }
/* The invalid ring is a real state, so it stays visible at rest. */
.ldh-edit-form .ldh-subject .uri-field.invalid { border-color: var(--danger-500); }
.ldh-edit-form .ldh-subject .uri-field.invalid > .msi.warn { opacity: 1; }
.ldh-edit-form .ldh-subject-tools { justify-self: end; }
/* The row's tools stay put. Property-row chrome fades because it repeats once per row and the repetition
   is the noise; there is one subject row, so there is nothing to suppress. The constructor action that
   used to sit here is now in the form footer (ldh:ConstructorActions), which is why this rule names only
   the copy button. */
.ldh-edit-form .ldh-subject .ldh-subject-copy { opacity: 1; }


/* The tabular markup reset lives in core surfaces.css with .ac-table. */

/* Ontology term list — <ul>/<li> of navigable terms. */
.ldh-onto-terms { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.ldh-onto-terms > li { display: flex; min-width: 0; }
.ldh-onto-terms > li > .ldh-onto-term { flex: 1; min-width: 0; }
/* The group label is a real heading now, so cancel the UA heading box. Weight
   stays regular — the eyebrow treatment is what carries it, not boldness. */
.ldh-onto-group-h { margin: 0; font-weight: var(--fw-regular); }


/* =========================================================================
   LIST MARKUP RESET
   Collections of navigable resources are real <ul>/<li>: the card grid and row
   list in a view block, the ResourceGrid, and the sidebar's tree / classes /
   other / latest-docs lists. `display: contents` on the <li> would keep the
   old layout with one line of CSS, but several engines drop the listitem role
   when it is applied — which is the whole point of the markup change. So the
   <li> IS the layout item and the anchor fills it.
   ========================================================================= */
/* .sb-classes and .ldh-tree are NOT in this list: both own a padding rule of
   their own, and this reset is appended LATER in the file at equal specificity
   (0,1,0), so listing them here silently zeroes their inset. Same trap as the
   doubled-class title rule — when a component rule and a blanket reset tie,
   file order decides, and the reset is always last. */
.ldh-grid-block, .ldh-list-block, .ldh-grid,
.sb-other, .mlist { list-style: none; margin: 0; padding: 0; }
/* The sidebar's own lists are NOT in this reset. `.sb-tree` no longer exists
   (renamed `.ldh-tree`), and the rail owns its own row vocabulary — `.tree-row`
   is the flex parent in the tree, `.sb-classes > li` in the class list. Listing
   them here re-broke alignment twice: a `> *` child stretch at (0,2,0) beats
   `.tree-spacer`'s (0,1,0) `width: 28px`, giving the spacer `flex: 1 1 0%` so it
   grew to 120px instead of holding its column. */
.ldh-grid-block > li, .ldh-list-block > li, .ldh-grid > li,
.sb-other > li, .mlist > li {
  display: flex; min-width: 0;
}
.ldh-grid-block > li > .card,
.ldh-list-block > li > .row,
.ldh-grid > li > .ldh-grid-card,
.sb-other > li > .sb-other-row,
.mlist > li > .mresult { flex: 1; min-width: 0; }
/* `.sb-other`'s rows render their own row element, so the li just has to let it
   span. Deliberately NOT extended to the tree or class lists: their rows carry
   a fixed-width spacer column, and a blanket `> *` stretch destroys it. */
.sb-other > li > * { flex: 1; min-width: 0; }


/* =========================================================================
   RESOURCE TYPE CHIPS — the rdf:type, shown in the block header.
   A type is what the resource IS, not one description among many: it selects
   the constructor, the header icon and the applicable SHACL shapes. So it sits
   with the identity rather than as a statement row, and is consumed out of the
   property list. Each chip LINKS to the class definition (another document);
   the class's constructor is class-level data and opens in a modal instead.
   Multiple types are ordinary in RDF, so this is a wrapping row of chips.
   ========================================================================= */
.ldh-types { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; min-width: 0; }
.ldh-type-chip {
  display: inline-flex; align-items: center; gap: 3px;
  height: 20px; padding: 0 7px 0 8px; box-sizing: border-box;
  background: var(--bg-accent-quiet); color: var(--ldh-blue-700);
  border: 1px solid color-mix(in srgb, var(--ldh-blue-700) 18%, transparent);
  border-radius: var(--r-pill);
  font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: var(--fw-medium);
  text-decoration: none; white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft), border-color var(--dur-fast) var(--ease-out-soft);
}
.ldh-type-chip:hover {
  background: var(--ldh-blue-100);
  border-color: color-mix(in srgb, var(--ldh-blue-700) 34%, transparent);
  text-decoration: none;
}
.ldh-type-chip:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
.ldh-type-chip .msi { font-size: 12px; opacity: 0.7; }
/* Quiet variant — a STATIC type label, not a link to the class definition.
   The accent chip is an affordance: it carries the north_east icon and opens
   the class document. Giving a non-interactive label the same accent would make
   "clickable" and "just a label" look identical, and eight accent pills down a
   dense row list shouts far louder than the row content. Same chip shape and
   metrics, quiet neutral ink, no hover response.

/* `.is-quiet` is the OUTGOING variant: the shipped bundle still emits
   `.ldh-type-chip is-quiet` on list rows until it recompiles, so these styles
   stay one cycle. The invariant is now that an rdf:type value renders as the
   core type Tag (.ac-tag.em-quiet.co-primary) wherever it lands, so this
   app-level variant has no incoming consumer and comes out next pass.

   The doubled class is deliberate, exactly as with `.ttl.ttl`: every skin
   patches `.ldh-type-chip` at `:root[data-retro…] .ldh-type-chip` (0,3,0) and
   the skin files load after app.css, so a plain `.ldh-type-chip.is-quiet`
   (0,2,0) loses and the label renders accent-coloured. */
:root .ldh-type-chip.is-quiet.is-quiet {
  background: var(--surface-2); color: var(--fg-muted);
  border-color: transparent;
  font-family: var(--font-mono); font-size: 10px;
}
:root .ldh-type-chip.is-quiet.is-quiet:hover {
  background: var(--surface-2); color: var(--fg-muted); border-color: transparent;
}


/* =========================================================================
   PAGED VIEW — rows scroll, pager stays put.
   The pager was a static block-flow sibling of the rows in an unconstrained
   .ldh-block-body, so it simply followed them off screen. Sticky would not fix
   it: .ldh-block is overflow:hidden and exactly as tall as its content, so a
   sticky child has no room to move.
   The fix is to give the ROWS a scroll box. The body becomes a flex column and
   the results region takes the free space; the pager is the column's last item
   and shrinks for no one, so it sits at the bottom of the container in both
   contexts — a page-flow block (capped height) and a height-constrained parent
   like the search modal (fills it).
   Scoped with :has(> .ldh-pager:not(:empty)) so an unpaged view is untouched.
   The :not(:empty) is what keeps that promise once a client renders the pager
   host up front and fills it asynchronously (see .ldh-pager:empty above): the
   host is then present on every view, paged or not, and a bare
   :has(> .ldh-pager) would cap and scroll bodies that never page. An empty host
   is display:none, so this reads as "lay the column out when there is a pager
   in it", which is what the rule always meant.

   A product may put its rows inside a results region of its own rather than
   directly in the body (LDH wraps them in .container-results so one element can
   be replaced per render). Such a wrapper needs no rule here: it is a scroll
   container, and a scroll container's automatic minimum size is zero, so it
   shrinks under the cap on its own — which is what the explicit min-height: 0
   below buys for a bare ul that is not one.
   ========================================================================= */
.ldh-block-body:has(> .ldh-pager:not(:empty)) {
  display: flex; flex-direction: column; min-height: 0;
  /* Something to scroll against in page flow. Inside an already-constrained
     parent this is the ceiling, not the height. */
  max-height: min(70vh, 620px);
}
.ldh-block-body:has(> .ldh-pager:not(:empty)) > .ldh-grid-block,
.ldh-block-body:has(> .ldh-pager:not(:empty)) > .ldh-list-block {
  flex: 1 1 auto;
  /* min-height:0 is required — a flex item defaults to min-height:auto and
     refuses to shrink below its content, so the scroll box never forms. */
  min-height: 0;
  overflow-y: auto;
}
/* Last in the column, never compressed, and opaque so rows cannot bleed
   through as they scroll under its top border. */
.ldh-block-body:has(> .ldh-pager:not(:empty)) > .ldh-pager {
  flex: 0 0 auto;
  background: var(--bg-card);
}
/* The toolbar is pinned at the top of the same column for the same reason. */
.ldh-block-body:has(> .ldh-pager:not(:empty)) > .ldh-view-toolbar { flex: 0 0 auto; }

/* A dialog hosting a paged view IS the constrained parent the cap above stands in for, so inside a
   flush modal body the column runs from the body down to the view: every wrapper between the two -
   a scroll region (the search dialog), a block row, a card - becomes a shrinkable column, and the
   view body drops the cap and takes the slack. The rows keep their own scroll box, and the pager,
   still the column's last unshrinkable item, pins to the bottom of the DIALOG rather than to a
   620px ceiling inside it. Selected with :has() rather than by naming the wrappers, because the kit
   cannot know what a consumer nests between its dialog and its view. */
.ac-modal-body.is-flush *:has(.ldh-block-body > .ldh-pager:not(:empty)) { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
.ac-modal-body.is-flush .ldh-block-body:has(> .ldh-pager:not(:empty)) { max-height: none; flex: 1 1 auto; }


/* =========================================================================
   BLOCK HEADER DENSITY
   One header serves every block, so nesting depth scales it rather than
   swapping it. `data-density="compact"` changes size and padding ONLY — the
   slots and their order are identical to a top-level header, because depth is
   not a type.
   (.ldh-nblock-head above is the outgoing markup and stays styled until the
   bundle carrying BlockHeader ships.)
   ========================================================================= */
.ldh-block-head[data-density="compact"] {
  padding: var(--sp-2) var(--sp-3);
  gap: var(--sp-2);
}
.ldh-block-head[data-density="compact"] .ldh-res-icon,
.ldh-block-head[data-density="compact"] .ldh-res-thumb {
  width: 26px; height: 26px; border-radius: var(--r-sm);
}
.ldh-block-head[data-density="compact"] .ldh-res-icon .msi { font-size: 16px; }
/* The doubled class is deliberate. EVERY skin sets the block title size at
   `:root[data-retro…] .ldh-block-head .ttl` — (0,4,0) — and the skin files load
   after app.css, so a matching (0,4,0) here loses the tie and a nested title
   renders at full 22px. Patching each skin would work today and break on the
   next skin added, because size at a given density is STRUCTURE (the component
   owns it), not theme. `.ttl.ttl` is (0,5,0) and wins for any skin, present or
   future. Font family and colour stay with the skin, which is theme. */
:root .ldh-block-head[data-density="compact"] .ttl.ttl {
  font-size: var(--fs-sm); font-weight: var(--fw-semibold); line-height: 1.3;
}
:root .ldh-block-head[data-density="compact"] .ttl--hero.ttl--hero {
  font-size: var(--fs-base); line-height: 1.3;
}
.ldh-block-head[data-density="compact"] .sub { font-size: var(--fs-xs); margin-top: 2px; }
.ldh-block-head[data-density="compact"] .ldh-res-desc { font-size: var(--fs-sm); }
.ldh-block-head[data-density="compact"] .ldh-type-chip {
  height: 18px; padding: 0 6px 0 7px; font-size: 10px;
}
.ldh-block-head[data-density="compact"] .ldh-type-chip .msi { font-size: 11px; }
.ldh-block-head[data-density="compact"] .actions .ac-iconbtn { width: 26px; height: 26px; }

/* =========================================================================
   CONTENT-SLOT CHROME — rail · slot bar · corner cluster · embedded card.

   A content slot is a CARRIER: an `ldh:Object` block that embeds another
   resource's card. The block contract is untouched — one block, one resource,
   one header — but a carrier's own content blocks (XHTML, Object) render
   headerless, so their chrome splits by the polarity the header always had:
   the drag grip left, everything actionable right.
   ========================================================================= */

/* The rail: a drag grip down the card's left edge, surfaced on hover/focus.
   It carries no buttons — they belong with every other actions cluster on the
   right. Note it is hit-testable even at opacity 0, which is why the slot bar
   has to sit above it. */
.ldh-block-rail {
  position: absolute; top: 0; left: 0; bottom: 0; width: 22px;
  display: flex; flex-direction: column; align-items: center;
  opacity: 0; z-index: 5;
  transition: opacity var(--dur-fast) var(--ease-out-soft);
}
.ldh-block:hover > .ldh-block-rail,
.ldh-block-rail:focus-within { opacity: 1; }
.ldh-block-rail > .ldh-bh-drag { flex: 1 1 auto; height: auto; width: 22px; margin-top: 0; }

/* The slot bar: the Object carrier's top-edge band, tinted apart from the
   embedded card's surface so the nesting is visible, and just tall enough for
   the right-aligned actions.
   z-index 6 is load-bearing: the rail's strip overlays the card's left edge and
   stays hit-testable at opacity 0, so without this the rail swallows the
   disclosure's clicks. */
.ldh-slot-bar {
  position: relative; z-index: 6;
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: 32px; padding: 0 var(--sp-2) 0 var(--sp-3);
  background: var(--surface-1);
  border-bottom: 1px solid var(--border-default);
  /* Fits any skin's card corners without the card clipping anything. */
  border-top-left-radius: inherit; border-top-right-radius: inherit;
}
/* details as a FLEX ROW, so [open] content lands beside the chevron rather
   than below it and the band height never changes. */
.ldh-slot-bar > details { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.ldh-slot-bar summary {
  display: inline-flex; align-items: center; cursor: pointer;
  list-style: none; color: var(--fg-hint); border-radius: var(--r-xs);
}
.ldh-slot-bar summary::-webkit-details-marker { display: none; }
.ldh-slot-bar summary:hover { color: var(--fg-2); }
.ldh-slot-bar summary:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
.ldh-slot-bar summary .msi { transition: transform var(--dur-fast) var(--ease-out-soft); }
.ldh-slot-bar details[open] summary .msi { transform: rotate(90deg); }
.ldh-slot-bar .ttl { font-size: var(--fs-sm); color: var(--fg-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ldh-slot-bar > .actions { margin-left: auto; display: flex; align-items: center; gap: var(--sp-1); }

/* The corner cluster: a headerless resource's own actions, anchored top-right.
   It stays put while its links popover is open, or dismissing the popover
   would fade out the control that opened it. */
.ldh-block-corner {
  position: absolute; top: var(--sp-3); right: var(--sp-3);
  display: flex; align-items: center; gap: var(--sp-1);
  opacity: 0; transition: opacity var(--dur-fast) var(--ease-out-soft);
}
/* A consumer that wraps its buttons in a div of its own flattens into the row. */
.ldh-block-corner > div:not(.links-nav) { display: contents; }
.ldh-block:hover > .ldh-block-corner,
.ldh-block > .ldh-block-corner:focus-within,
.ldh-block > .ldh-block-corner:has(.is-open) { opacity: 1; }
/* Reveal-on-hover has no reveal where there is no hover. On a touch device none of the three
   conditions above can fire before the user has already hit the thing they cannot see, so the
   cluster stays at opacity 0 with pointer-events auto: Backlinks, Copy URI and Edit are
   permanently invisible AND permanently tappable. Measured on an iPhone 13 profile at 390px, the
   cluster's box is 262-366 and the block title's is 36-354 - about 90px of the title's last words
   are a hidden tap target that opens the backlinks panel.
   Taking it out of the overlay and into flow answers both halves at once: the controls become
   visible and reachable, and nothing sits underneath them to collide with. The absolute placement
   exists so the cluster can hover over content without shifting it - a trade only worth making
   when it can actually hide again. */
@media (hover: none) {
  .ldh-block > .ldh-block-corner {
    position: static;
    opacity: 1;
    justify-content: flex-end;
    padding: var(--sp-2) var(--sp-3) 0;
  }
}
/* The cluster is absolutely positioned, so it reserves no space — and the one
   case it exists for is a headerless block whose prose fills the width, so the
   buttons land on the first line at exactly the moment you reach for them.
   Reserve a gutter only where a cluster is present; blocks without one keep
   their normal padding. Derived, not magic: right offset + two 32px buttons +
   their gap + breathing room. Override the token for a wider cluster. */
/* Derived, not magic: right offset + two 32px buttons + their gap + breathing
   room. Declared on :root so it registers as a design-system token and stays
   overridable for a wider cluster — a component-scoped declaration would be
   neither. */
:root { --block-corner-gutter: calc(var(--sp-3) + 2 * 32px + var(--sp-1) + var(--sp-3)); }
.ldh-block:has(> .ldh-block-corner) > .ldh-block-body {
  padding-right: var(--block-corner-gutter);
}
/* 92px of the 342px a phone has left is a quarter of the body spent on clearance for two buttons.
   The corner cluster keeps its position - it is absolutely placed, so it does not need the gutter
   to exist - and the body reclaims the width; what the buttons overlap narrow is the body's own
   padding, not its text. */
@container (max-width: 520px) {
  .ldh-block:has(> .ldh-block-corner) > .ldh-block-body { padding-right: var(--sp-4); }
}

/* The embedded card — an Object carrier's `rdf:value` resource, and the one
   header-bearing surface in the slot. Flattened onto the carrier (no surface,
   shadow or lift of its own) with an inset left hairline as the embedding
   signal, so it reads differently from the same resource standalone.
   :root matches the skins' base and hover specificity, so load order wins —
   whether a card is flattened is STRUCTURE, which the component owns, not
   theme. Each skin also carries the hover-lift exemption. */
:root .ldh-block.is-embedded {
  background: transparent; border: 0; /* inherit, not 0: the carrier is rounded and no longer clips, so a
     square-cornered inset hairline escapes past its corners. */
  border-radius: inherit;
  box-shadow: inset 2px 0 0 var(--border-default);
}
:root .ldh-block.is-embedded:hover {
  transform: none; box-shadow: inset 2px 0 0 var(--border-default);
}
/* The hover lift is an ELEVATION affordance, so a block with no elevation must
   not take it: on a transparent prose block the 1px translate reads as the text
   wiggling. Sits beside the skins' existing is-quiet hover-shadow exemptions. */
:root .ldh-block.is-quiet:hover { transform: none; }

/* An embedded card only meets a carrier CORNER on edges where it is flush, so
   `border-radius: inherit` is right at the bottom always, and at the top only
   when nothing sits above it. A slot bar occupies that edge, and there the
   inherited curve pulls the hairline away from the bar's bottom border as a
   detached arc — the junction has to read as one continuous left edge.
   Tested on the CAUSE (a slot bar is present), not on position: the rail is a
   DOM sibling that precedes the card while being absolutely positioned, so
   :first-child is false even in carriers where the card IS flush at the top.
   Doubled class for (0,5,0): each skin's is-embedded rule is (0,4,0) and loads
   after app.css — same precedent as `.ttl.ttl` for compact density. */
.ldh-block:has(> .ldh-slot-bar) > .ldh-block.is-embedded.is-embedded {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

/* Drag handle — the rail's grip (and formerly a header slot). */
.ldh-bh-drag {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; flex-shrink: 0; margin-top: 2px;
  color: var(--ink-4); cursor: grab; border-radius: var(--r-xs);
  transition: background var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft);
}
.ldh-bh-drag:hover { background: var(--bg-hover); color: var(--fg-2); }
.ldh-bh-drag:active { cursor: grabbing; }
.ldh-bh-drag:focus-visible { outline: 0; box-shadow: var(--focus-ring); }


/* No provenance line exists. A block shell renders the RESOURCE — title, type
   chips, description, depiction, and the content itself. Where that content
   came from (a query, a container, a row count) is not part of the resource, so
   it gets no slot in the shell, header or footer. If the query is a property of
   the resource, it shows as a statement in the resource's own description. */


/* =========================================================================
   PIVOT BAR — parallax navigation, its own row under the facet/sort toolbar.
   One pill per predicate PER DIRECTION: pivots are the primary way to travel a
   graph, so the whole set is offered at once rather than behind a picker. The
   arrow leads the label, and forward pills group before backward ones.

   A DISCLOSURE, closed by default. The set is worth showing whole, but not at
   the cost of the results: a pivot is onward navigation rather than an input to
   the query above it, and a second full pill row (wrapping to a third on a
   narrow card) pushed the rows themselves below the fold. Closed it states what
   it holds and how much of it; open it is the flat bar it always was.
   ========================================================================= */
.ldh-pivot-bar {
  /* Same tint and same dashed divider as .ldh-view-toolbar, and now the same padding,
     so the two read as one banded control header stacked in two rows - and so the pivot
     lead sits directly under the filter lead rather than a few pixels off it.

     It was a <details>, closed by default, because a full pill row pushed the results
     below the fold. The card header's controls toggle now keeps the whole header off the
     card until it is asked for, so a disclosure inside it only meant asking twice; what
     is left is the flat bar it always was. */
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-6);
  background: var(--surface-1);
  border-bottom: 1px dashed var(--border-default);
  min-width: 0;
}
/* The pills are laid out by the BAR, not by their own container: a flex item that is
   itself a flex container takes the max-content width of its whole unwrapped line, so
   once the pills outgrow the row they wrap as one unit and leave the lead glyph alone on
   the line above. display: contents hands them to the row, which already carries the
   align-items, flex-wrap and gap the host was repeating - the same treatment that put the
   glyph back on the first chip's line. The element stays: it is the id the chip renderer
   replaces the content of, and the role=group that names them. */
.ldh-pivot-pills { display: contents; }


.ldh-pivot-pill {
  /* Metrically identical to .facet-pill: same height, padding and gap. A pivot
     and a facet are both "one tap changes the result set", so they should not
     differ in weight. */
  display: inline-flex; align-items: center; gap: 6px;
  height: 28px; padding: 0 10px; box-sizing: border-box;
  background: var(--surface-0);
  border: 1px solid var(--border-default); border-radius: var(--r-pill);
  color: var(--fg-1); cursor: pointer;
  font-family: var(--font-sans); font-size: var(--fs-sm); font-weight: var(--fw-medium);
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out-soft), border-color var(--dur-fast) var(--ease-out-soft), color var(--dur-fast) var(--ease-out-soft);
}
.ldh-pivot-pill:hover {
  background: var(--bg-accent-quiet);
  border-color: var(--ldh-blue-400);
  color: var(--ldh-blue-700);
}
.ldh-pivot-pill:active { background: var(--ldh-blue-100); }
.ldh-pivot-pill:focus-visible { outline: 0; box-shadow: var(--focus-ring); }
/* The arrow is the direction, so it carries the accent, not the label. */
.ldh-pivot-pill .msi { color: var(--ldh-blue-600); flex-shrink: 0; }
.ldh-pivot-pill:hover .msi { color: var(--ldh-blue-700); }
.ldh-pivot-pill .lbl { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* The intermediate "Pivot" DROPDOWN (.ldh-pivot / -trigger / -pop / -h) is
   gone, along with a malformed comment left beside it whose body was sitting
   in the stylesheet as raw CSS. A flat bar shows every hop at once, which is
   the point of it. */

/* =========================================================================
   §16 LEFTOVERS — the two states that kept legacy names because the kit had
   not named them. Both now follow the kit's `is-*` convention.
   ========================================================================= */

/* 1 · BLOCK LOADING, COLLAPSED.
   Distinct from the skeleton loading state (.ldh-block-loading), which is for a
   block whose SHAPE is known — a table, a chart — and mirrors it. This is the
   other case: a block still resolving what it even is, collapsed to a single
   progress strip under its header.

   The pair LDH carried as "progress + active" is one state with one modifier:
     .ldh-block.is-loading            → the block is resolving
     .ldh-block.is-loading[aria-busy] → and is the one being awaited
   `aria-busy` is the standard attribute for exactly this, so the second class
   was redundant.

   The strip replaces the body rather than sitting above it: a header plus a bar
   is an honest "nothing yet", whereas a bar above empty body padding reads as a
   block that has loaded and is empty. */
.ldh-block.is-loading > .ldh-block-body { display: none; }
.ldh-block-bar {
  height: 2px; overflow: hidden;
  background: var(--surface-3);
  border-radius: var(--r-pill);
  margin: 0 var(--sp-6) var(--sp-4);
}
.ldh-block.is-loading > .ldh-block-bar::after {
  content: ""; display: block; height: 100%; width: 35%;
  background: var(--bg-accent); border-radius: inherit;
  animation: ldh-block-bar 1.3s var(--ease-in-out) infinite;
}
@keyframes ldh-block-bar { 0% { margin-left: -35%; } 100% { margin-left: 100%; } }
/* Indeterminate by default; a known percentage sets --p and stops the sweep. */
.ldh-block.is-loading > .ldh-block-bar[style*="--p"]::after {
  width: var(--p); animation: none; margin-left: 0;
  transition: width var(--dur-slow) var(--ease-out-soft);
}
@media (prefers-reduced-motion: reduce) {
  .ldh-block.is-loading > .ldh-block-bar::after { animation: none; width: 100%; opacity: 0.4; }
}

/* 2 · SIDEBAR DOCUMENT-TREE ACTIVE ITEM.
   `is-active`, matching the tab strip's item state — it is the same statement
   ("this row is the document you are looking at") and should not have two
   names. LDH's `li.active` renames.
   The state goes on the LI so the rail and background span the whole row
   including its disclosure control, exactly as with the tabs.
   Scoped to `.sb-other`: the tree and class lists declare their own is-active
   rules next to the rest of their vocabulary, and a `> *` descendant here would
   also recolour their spacer column. */
.sb-other > li.is-active {
  background: var(--bg-selected);
  border-radius: var(--r-sm);
  box-shadow: inset 2px 0 0 var(--fg-selected);
}
.sb-other > li.is-active > * { color: var(--fg-selected); font-weight: var(--fw-medium); }
