/* ============================================================================
   Page-level styles (non-scoped)
   ---------------------------------------------------------------------------
   Consolidated here because Blazor scoped CSS adds a [b-xyz] attribute to
   every selector, which prevents ancestor selectors like `[data-bs-theme=dark]
   .thing` from matching (the <html> element has no scope attribute). Keeping
   page styles global lets us use those ancestor selectors freely and also
   sidesteps static-asset bundle caching issues we hit earlier.

   Organized by page, top-down in the user journey.
   ============================================================================ */


/* ============================================================================
   Accessibility utilities
   ============================================================================ */

.visually-hidden {
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    padding: 0 !important;
    margin: -1px !important;
    overflow: hidden !important;
    clip: rect(0, 0, 0, 0) !important;
    white-space: nowrap !important;
    border: 0 !important;
}


/* ============================================================================
   App Shell — sidebar + main area
   ============================================================================ */

.kmila-shell {
    --sidebar-w: var(--sidebar-width, 260px);
    --sidebar-w-collapsed: 72px;
    position: relative;
    display: grid;
    grid-template-columns: var(--sidebar-w) 1fr;
    min-height: 100vh;
    min-height: 100dvh;
    background-color: var(--surface-0);
    transition: grid-template-columns var(--duration-base) var(--ease-standard);
}

.kmila-shell:has(.kmila-sidebar--collapsed) {
    grid-template-columns: var(--sidebar-w-collapsed) 1fr;
}

.kmila-shell__main {
    min-width: 0;                /* prevent children from overflowing the grid track */
    display: flex;
    flex-direction: column;
    /* Pages with short content (Privacy, Logs end-state, last-step lessons)
       used to bottom-out mid-viewport with the footer floating up to meet
       them. The grid parent's `min-height: 100vh` doesn't always stretch
       the grid item on Android WebView, so we ALSO pin a viewport-based
       min-height directly on .kmila-shell__main as a redundant guarantee. */
    min-height: 100vh;
    min-height: 100dvh;
}

/* Article fills the available main area so the footer is pinned to the
   viewport bottom even when its body is short. flex-grow:1 takes the
   leftover space, basis:auto keeps natural sizing on long pages. */
.kmila-shell__main > .content {
    flex: 1 0 auto;
    /* Reserve the iOS home-indicator via env(). On desktop env() collapses
       to 0, no effect. Android WebView reports 0 for `safe-area-inset-bottom`
       (viewport-fit=cover doesn't populate the inset when the app doesn't
       draw edge-to-edge — Xiaomi HyperOS confirmed 2026-08-02). Below on
       touch devices we add a 12 px floor so the last row of any page
       clears the gesture-nav strip on Android too. */
    padding-bottom: env(safe-area-inset-bottom, 0);
}
@media (pointer: coarse) {
    .kmila-shell__main > .content {
        padding-bottom: max(env(safe-area-inset-bottom, 0px), 12px);
    }
}

/* Slim footer at the bottom of every page. Lives inside .kmila-shell__main
   so it scrolls with the page, not pinned. Holds the Privacy link. */
.kmila-shell__footer {
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--border-subtle);
    color: var(--text-muted);
    font-size: var(--fs-xs);
    display: flex;
    justify-content: flex-end;
    gap: var(--space-3);
    background: transparent;
}

/* F-1 (editor-mobile audit 2026-06-29): hide the footer on the Editor route
   on phones. It would otherwise eat a band between the editor body and the
   RunBar / dock — Privacy link is still reachable from /privacy + the
   profile menu's Account settings. */
@media (max-width: 768px) {
    .kmila-shell__main:has(.editor-page) .kmila-shell__footer { display: none; }
    /* R2-1 (editor-mobile audit 2026-06-29 round 2): drop Bootstrap's px-4
       horizontal padding from the article when it hosts the editor on
       phone. The padding wastes ~10 % of the horizontal viewport on a
       1080 px-wide phone — the editor card already has its own border +
       radius, so it doesn't need the gutter. */
    .kmila-shell__main:has(.editor-page) > .content {
        padding-left: var(--space-2) !important;
        padding-right: var(--space-2) !important;
    }
}

.kmila-shell__footer a {
    color: var(--text-muted);
    text-decoration: none;
}

.kmila-shell__footer a:hover {
    color: var(--text-strong);
    text-decoration: underline;
}


/* ============================================================================
   Sidebar
   ============================================================================ */

.kmila-sidebar {
    position: sticky;
    top: 0;
    align-self: start;
    height: 100vh;
    height: 100dvh;
    display: flex;
    flex-direction: column;
    background: linear-gradient(180deg, var(--kmila-primary-dark) 0%, var(--kmila-primary) 100%);
    color: rgba(255, 255, 255, 0.92);
    border-right: 1px solid rgba(255, 255, 255, 0.06);
    z-index: var(--z-sidebar);
    overflow: hidden;
    transition: width var(--duration-base) var(--ease-standard);
}

/* RTL: border flips to the opposite edge */
[dir="rtl"] .kmila-sidebar {
    border-right: none;
    border-left: 1px solid rgba(255, 255, 255, 0.06);
}


/* --- Brand / top row --- */

.kmila-sidebar__brand {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-4);
    text-decoration: none;
    color: #ffffff;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
    min-height: var(--topbar-height);
    flex-shrink: 0;
}

.kmila-sidebar__brand:hover {
    color: #ffffff;
    text-decoration: none;
    background-color: rgba(255, 255, 255, 0.04);
}

.kmila-sidebar__logo {
    width: 40px;
    height: 40px;
    object-fit: contain;
    flex-shrink: 0;
    filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.3));
}

.kmila-sidebar__wordmark {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    opacity: 1;
    transition: opacity var(--duration-fast) var(--ease-standard);
}

.kmila-sidebar__name {
    font-size: var(--fs-md);
    font-weight: var(--fw-bold);
    letter-spacing: var(--tracking-tight);
    color: #ffffff;
    line-height: 1;
}

.kmila-sidebar__version {
    display: inline-flex;
    align-items: center;
    width: fit-content;
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    font-weight: var(--fw-medium);
    color: rgba(255, 255, 255, 0.7);
    padding: 2px 6px;
    background-color: rgba(255, 255, 255, 0.1);
    border-radius: var(--radius-pill);
    letter-spacing: 0;
    line-height: 1;
}


/* --- Nav area --- */

.kmila-sidebar__nav {
    flex: 1 1 auto;
    overflow-y: auto;
    overflow-x: hidden;
    padding: var(--space-3) 0 var(--space-5);
    scrollbar-gutter: stable;
}

.kmila-sidebar__section + .kmila-sidebar__section {
    margin-top: var(--space-4);
}

.kmila-sidebar__section-label {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-2) var(--space-4);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    font-family: var(--font-mono);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.55);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.kmila-sidebar__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    padding: 0 6px;
    height: 18px;
    background-color: rgba(255, 255, 255, 0.15);
    color: #ffffff;
    border-radius: var(--radius-pill);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    font-family: var(--font-sans);
    letter-spacing: 0;
}


/* --- Nav items --- */

/* Sidebar nav items follow Family 15 (underline-on-hover · persistent
   when active) but rotated 90° for the vertical sidebar context: the
   __indicator span on the left is the "underline" — it grows from 0% to
   100% height on hover and stays at 100% when .is-active. No pill bg
   highlight; just the indicator + accent tint. Dark-sidebar text colors
   are kept (white-ish) since the standard --text-* tokens would be
   illegible against the slate gradient bg. */

.kmila-nav-item {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-4);
    margin: 2px var(--space-2);
    border-radius: var(--radius-sm);
    color: rgba(255, 255, 255, 0.82);
    text-decoration: none;
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    line-height: 1.2;
    min-height: 40px;
    white-space: nowrap;
    overflow: hidden;
    transition: color var(--duration-fast) var(--ease-standard);
}

.kmila-nav-item:hover,
.kmila-nav-item:focus-visible { color: #ffffff; text-decoration: none; }

.kmila-nav-item:focus-visible {
    outline: 2px solid var(--kmila-accent);
    outline-offset: -2px;
}

.kmila-nav-item.is-active {
    color: #ffffff;
    font-weight: var(--fw-semibold);
}

/* Vertical "underline" (Family 15 rotated): the indicator scales from
   0 to full height on hover, persists at 100% when active. transform
   instead of width avoids layout shift. */
.kmila-nav-item__indicator {
    position: absolute;
    left: 0;
    top: 8px;
    bottom: 8px;
    width: 3px;
    border-radius: 0 var(--radius-xs) var(--radius-xs) 0;
    background-color: var(--kmila-accent);
    transform: scaleY(0);
    transform-origin: center top;
    transition: transform var(--duration-base) var(--ease-standard);
}

[dir="rtl"] .kmila-nav-item__indicator {
    left: auto;
    right: 0;
    border-radius: var(--radius-xs) 0 0 var(--radius-xs);
}

.kmila-nav-item:hover .kmila-nav-item__indicator,
.kmila-nav-item:focus-visible .kmila-nav-item__indicator,
.kmila-nav-item.is-active .kmila-nav-item__indicator { transform: scaleY(1); }

.kmila-nav-item__icon {
    font-size: var(--fs-md);
    width: 1.15rem;
    text-align: center;
    flex-shrink: 0;
    color: rgba(255, 255, 255, 0.75);
    transition: color var(--duration-fast) var(--ease-standard);
}

.kmila-nav-item:hover .kmila-nav-item__icon,
.kmila-nav-item.is-active .kmila-nav-item__icon {
    color: var(--kmila-accent);
}

.kmila-nav-item__label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Project items use a denser row */
.kmila-nav-item--project {
    min-height: 36px;
    padding-top: var(--space-1);
    padding-bottom: var(--space-1);
    font-weight: var(--fw-regular);
}


/* --- Footer / collapse toggle --- */

.kmila-sidebar__footer {
    flex-shrink: 0;
    padding: var(--space-2);
    border-top: 1px solid rgba(255, 255, 255, 0.08);
}

.kmila-sidebar__collapse-btn {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    padding: var(--space-2) var(--space-3);
    background-color: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    color: rgba(255, 255, 255, 0.65);
    font-size: var(--fs-sm);
    font-family: inherit;
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-standard),
                color var(--duration-fast) var(--ease-standard);
}

.kmila-sidebar__collapse-btn:hover {
    background-color: rgba(255, 255, 255, 0.08);
    color: #ffffff;
}

.kmila-sidebar__collapse-btn:focus-visible {
    outline: 2px solid var(--kmila-accent);
    outline-offset: -2px;
}


/* ============================================================================
   Collapsed (rail) mode — desktop
   ============================================================================ */

.kmila-sidebar--collapsed .kmila-sidebar__brand {
    padding-left: var(--space-2);
    padding-right: var(--space-2);
    justify-content: center;
}

.kmila-sidebar--collapsed .kmila-sidebar__wordmark {
    display: none;
}

/* In rail mode the section label becomes a thin divider line rather than
   partial text. Works for both single-span and label+count label shapes. */
.kmila-sidebar--collapsed .kmila-sidebar__section-label {
    position: relative;
    height: 1px;
    padding: 0;
    margin: var(--space-3) var(--space-3);
    background-color: rgba(255, 255, 255, 0.08);
    overflow: hidden;
    font-size: 0;
    color: transparent;
}

.kmila-sidebar--collapsed .kmila-sidebar__section-label > * {
    display: none;
}

.kmila-sidebar--collapsed .kmila-nav-item {
    justify-content: center;
    padding-left: var(--space-2);
    padding-right: var(--space-2);
}

.kmila-sidebar--collapsed .kmila-nav-item__label {
    display: none;
}

.kmila-sidebar--collapsed .kmila-sidebar__collapse-btn {
    justify-content: center;
}

.kmila-sidebar--collapsed .kmila-sidebar__collapse-btn .kmila-nav-item__label {
    display: none;
}


/* --- Tooltip on hover in collapsed state --- */

.kmila-sidebar--collapsed .kmila-nav-item[data-tooltip]:hover::after {
    content: attr(data-tooltip);
    position: absolute;
    left: calc(100% + var(--space-2));
    top: 50%;
    transform: translateY(-50%);
    z-index: var(--z-popover);
    padding: var(--space-1) var(--space-3);
    background-color: var(--surface-inverse);
    color: var(--text-inverse);
    font-size: var(--fs-xs);
    font-weight: var(--fw-medium);
    white-space: nowrap;
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-2);
    pointer-events: none;
}

[dir="rtl"] .kmila-sidebar--collapsed .kmila-nav-item[data-tooltip]:hover::after {
    left: auto;
    right: calc(100% + var(--space-2));
}


/* ============================================================================
   Mobile drawer (< 641 px) — off-canvas with backdrop
   ============================================================================ */

@media (max-width: 640.98px) {
    .kmila-shell {
        grid-template-columns: 1fr;       /* sidebar leaves the grid flow */
    }

    .kmila-shell:has(.kmila-sidebar--collapsed) {
        grid-template-columns: 1fr;       /* collapsed state has no meaning on mobile */
    }

    .kmila-sidebar {
        position: fixed;
        top: 0;
        left: 0;
        width: min(320px, 88vw);
        height: 100dvh;
        transform: translateX(-105%);
        box-shadow: var(--shadow-3);
    }

    [dir="rtl"] .kmila-sidebar {
        left: auto;
        right: 0;
        transform: translateX(105%);
    }

    .kmila-sidebar--open {
        transform: translateX(0);
    }
    /* RTL needs its own --open rule because the [dir="rtl"] selector above is
       more specific than the bare `.kmila-sidebar--open`. Without this the
       drawer stays parked at translateX(105%) and the hamburger flip works
       but the panel never slides into view. */
    [dir="rtl"] .kmila-sidebar--open {
        transform: translateX(0);
    }

    /* Force expanded visuals in the drawer even if the user last left the
       desktop in collapsed mode. */
    .kmila-sidebar--collapsed {
        width: min(320px, 88vw);
    }
    .kmila-sidebar--collapsed .kmila-sidebar__wordmark,
    .kmila-sidebar--collapsed .kmila-nav-item__label,
    .kmila-sidebar--collapsed .kmila-sidebar__section-label > span:first-child,
    .kmila-sidebar--collapsed .kmila-sidebar__collapse-btn .kmila-nav-item__label {
        display: inline-block;
    }
    .kmila-sidebar--collapsed .kmila-nav-item,
    .kmila-sidebar--collapsed .kmila-sidebar__brand,
    .kmila-sidebar--collapsed .kmila-sidebar__section-label {
        justify-content: flex-start;
    }

    /* Collapse toggle is desktop-only UX */
    .kmila-sidebar__footer { display: none; }

    .kmila-sidebar__backdrop {
        position: fixed;
        inset: 0;
        background-color: rgba(15, 23, 42, 0.45);
        z-index: calc(var(--z-sidebar) - 1);
        animation: kmilaFade var(--duration-base) var(--ease-standard);
    }
}

@keyframes kmilaFade {
    from { opacity: 0; }
    to   { opacity: 1; }
}


/* ============================================================================
   Scrollbar inside dark sidebar — visible on dark background
   ============================================================================ */

.kmila-sidebar__nav::-webkit-scrollbar-thumb {
    background-color: rgba(255, 255, 255, 0.18);
}
.kmila-sidebar__nav::-webkit-scrollbar-thumb:hover {
    background-color: rgba(255, 255, 255, 0.3);
}
.kmila-sidebar__nav {
    scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}


/* ============================================================================
   TopBar
   ============================================================================ */

.kmila-topbar {
    position: sticky;
    top: 0;
    /* Must outrank the bottom-sheet dock (--z-drawer = 1020) so the profile
       menu / config-pill popovers — which inherit this stacking context —
       paint above the dock on mobile. Stays below --z-sidebar (1030) so the
       sidebar drawer still covers the topbar when open, and below --z-modal
       (1050) so dialogs/backdrops cover everything. */
    z-index: calc(var(--z-drawer) + 1);
    background-color: var(--surface-2);
    border-bottom: 1px solid var(--border-subtle);
    min-height: var(--topbar-height);
    box-shadow: var(--shadow-1);
}

.kmila-topbar__inner {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-4);
    min-height: var(--topbar-height);
}

.kmila-topbar__start {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex: 1 1 auto;
    min-width: 0;
}

.kmila-topbar__end {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-shrink: 0;
}


/* --- Hamburger button (mobile only) --- */

/* Visual styles come from .kmila-btn-icon (buttons.css). This selector is
   reserved purely for the show/hide media-query gate that hides the
   hamburger on desktop and reveals it on mobile.
   Chain with .kmila-btn-icon so we beat its own display:inline-flex
   regardless of which stylesheet loads first. */
.kmila-btn-icon.kmila-topbar__menu-btn { display: none; }
@media (max-width: 640.98px) {
    .kmila-btn-icon.kmila-topbar__menu-btn { display: inline-flex; }
}


/* ============================================================================
   Breadcrumbs
   ============================================================================ */

.kmila-breadcrumbs {
    min-width: 0;
    flex: 1 1 auto;
    overflow: hidden;
}

.kmila-breadcrumbs ol {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 0;
    min-width: 0;
}

.kmila-breadcrumbs__item {
    display: inline-flex;
    align-items: center;
    min-width: 0;
    max-width: 260px;
}

.kmila-breadcrumbs__link,
.kmila-breadcrumbs__current {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-2);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    color: var(--text-muted);
    text-decoration: none;
    border-radius: var(--radius-xs);
    min-width: 0;
}

.kmila-breadcrumbs__link > span,
.kmila-breadcrumbs__current > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* Mobile: give the *current* (terminal) crumb more horizontal room than
       its ancestors so route labels like "Practice" / "Settings" don't show
       as "Pra…" / "Setti…". Ancestors collapse to icon-only first. */
    max-width: 16ch;
}
.kmila-breadcrumbs__current > span { max-width: 26ch; }
@media (max-width: 600px) {
    /* On phones only the last two crumbs render (rule below at 640.98px) so we
       can afford to let those labels show their full text. Drop the inner-span
       max-width entirely; the outer `.kmila-breadcrumbs__item` 200px cap (set
       inside the 640.98 px block below) is the only safety net needed. */
    .kmila-breadcrumbs__link > span,
    .kmila-breadcrumbs__current > span { max-width: none; }
}

.kmila-breadcrumbs__link:hover {
    color: var(--text-strong);
    background-color: var(--surface-3);
    text-decoration: none;
}

.kmila-breadcrumbs__current {
    color: var(--text-strong);
    font-weight: var(--fw-semibold);
}

.kmila-breadcrumbs__sep {
    display: inline-flex;
    align-items: center;
    color: var(--text-subtle);
    padding: 0 var(--space-1);
    font-size: var(--fs-sm);
}

[dir="rtl"] .kmila-breadcrumbs__sep .bi-chevron-right::before {
    /* Mirror chevron in RTL */
    content: "\F284"; /* bootstrap icons chevron-left */
}

/* On phones the breadcrumb container shares ~240px with the hamburger and
   the topbar's right cluster (profile + config pill). Two crumbs would
   compete for that space and both ellipsize, so collapse to the current
   crumb only — that's the "you are here" signal the user needs after
   navigating, and the back gesture / sidebar replaces the trail. */
@media (max-width: 640.98px) {
    .kmila-breadcrumbs__item:not(.is-current) { display: none; }
    .kmila-breadcrumbs__sep { display: none; }
    .kmila-breadcrumbs__item { max-width: none; }
    /* With only the current crumb visible, the inner text must not shrink
       under `overflow:hidden + ellipsis` because there's no second crumb
       competing for space. Otherwise `Conceptos` → `Concep…` on a 375 px
       viewport (see audit 2026-06-14). Allow the text to take its natural
       width; the topbar's flex layout will let the end cluster shrink first. */
    .kmila-breadcrumbs__current > span,
    .kmila-breadcrumbs__link > span {
        overflow: visible;
        text-overflow: clip;
        max-width: none;
    }
}

/* F-081 (Round 9 2026-08-05, CDP-discovered) — at xl (1.20) text scale on
   ≤ 640 px viewports the breadcrumb text overflows even the topbar's shrunk
   allotment, and the 2026-06-14 `clip` rule leaves it silently truncated at
   the wrong edge (e.g. "ample · Demo board" instead of "Sample · Demo boar…"
   in AR + LTR project name). At normal 1.0x scale the text fits, so this
   never fires. `calc(1 * var(...))` is a load-bearing feature-test: if the
   var is unset (LTR pre-Blazor-boot) the calc evaluates and this rule
   scopes cleanly to the moment the scale is elevated. Falls back to
   ellipsis so the truncation is at least indicative and not silent. */
@media (max-width: 640.98px) {
    :root[style*="--user-text-scale: 1.2"] .kmila-breadcrumbs__current > span,
    :root[style*="--user-text-scale: 1.20"] .kmila-breadcrumbs__current > span {
        overflow: hidden;
        text-overflow: ellipsis;
    }
}

/* F-089 (Round 10 2026-08-08, CDP-discovered) — at viewports below 380 CSS px
   (physical 560 dpi Xiaomi 13T Pro without density override → 308 CSS px, or
   any user who raises system density, or split-screen), "Sample · Demo board"
   silent-clips to just "Demo board" at md scale because the F-081 rule above
   is scoped to xl only. Below the 375 px audit threshold that motivated the
   2026-06-14 clip decision, every crumb is at real risk of clipping regardless
   of text scale, so surface the truncation with an ellipsis. The 380 px
   breakpoint gives a small buffer above the 375 px threshold so ordinary
   375-411 px phones keep the deliberate 2026-06-14 clip. */
@media (max-width: 380px) {
    .kmila-breadcrumbs__current > span {
        overflow: hidden;
        text-overflow: ellipsis;
    }
}

/* Tablet sweet-spot 641–820 px: the full 3-crumb trail still overflows the
   topbar once the ConfigurationsPill + ProfileMenu cluster claims its slice,
   producing mid-word truncation like "Ejemplo · Contad…" (audit 2026-06-14
   D-01). Drop the leading "Home" crumb here so we keep a 2-step trail
   ("Projects · {ProjectName}") with room to render unclipped. */
@media (min-width: 641px) and (max-width: 820px) {
    /* Hide the first item (Home) and its trailing separator. The first
       separator lives inside the first <li>, so hiding that item also hides
       its separator without needing a :nth-child sibling rule. */
    .kmila-breadcrumbs__item:first-child { display: none; }
    /* Tighten the inner-span max-width so the surviving crumbs can shrink
       gracefully if needed; the current crumb still gets the lion's share. */
    .kmila-breadcrumbs__link > span { max-width: 12ch; }
    .kmila-breadcrumbs__current > span { max-width: none; }
}


/* ============================================================================
   FPGA picker (native <details> popover)
   ============================================================================ */

.kmila-fpga-picker {
    position: relative;
}

.kmila-fpga-picker[open] > .kmila-fpga-picker__panel {
    display: flex;
}

.kmila-fpga-picker__summary {
    list-style: none;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    color: var(--text-default);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    line-height: 1.2;
    max-width: 260px;
    user-select: none;
    transition: background-color var(--duration-fast) var(--ease-standard),
                border-color var(--duration-fast) var(--ease-standard);
    /* Tap target — pill geometry shrinks to ~32px tall by default; bump to
       the 44px accessibility minimum on touch so the "Select FPGA" chip
       inside the profile menu (a critical Run-flow gate) is thumb-friendly
       instead of fiddly. */
    min-height: var(--tap-target);
}

.kmila-fpga-picker__summary::-webkit-details-marker { display: none; }
.kmila-fpga-picker__summary::marker { display: none; content: ""; }

.kmila-fpga-picker__summary:hover {
    background-color: var(--surface-2);
    border-color: var(--border-default);
}

.kmila-fpga-picker[open] .kmila-fpga-picker__summary {
    border-color: var(--kmila-accent);
    box-shadow: var(--shadow-focus);
}

.kmila-fpga-picker__text {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-2);
    min-width: 0;
    overflow: hidden;
}

.kmila-fpga-picker__model {
    color: var(--text-strong);
    font-weight: var(--fw-semibold);
    font-family: var(--font-mono);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.kmila-fpga-picker__family {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
}

.kmila-fpga-picker__placeholder {
    color: var(--text-muted);
    font-style: italic;
}

.kmila-fpga-picker__chevron {
    margin-left: auto;
    color: var(--text-muted);
    transition: transform var(--duration-fast) var(--ease-standard);
}

.kmila-fpga-picker[open] .kmila-fpga-picker__chevron {
    transform: rotate(180deg);
}


/* --- Panel --- */

.kmila-fpga-picker__panel {
    display: none;
    position: absolute;
    top: calc(100% + var(--space-2));
    right: 0;
    left: auto;
    width: min(380px, 90vw);
    max-height: 420px;
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    flex-direction: column;
    overflow: hidden;
    z-index: var(--z-dropdown);
    animation: kmilaPop var(--duration-fast) var(--ease-standard);
}

[dir="rtl"] .kmila-fpga-picker__panel {
    right: auto;
    left: 0;
}

@keyframes kmilaPop {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: translateY(0); }
}

.kmila-fpga-picker__search {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
    color: var(--text-muted);
}

.kmila-fpga-picker__search .form-control {
    border: none;
    padding: 0;
    background: transparent;
    box-shadow: none;
}

.kmila-fpga-picker__search .form-control:focus {
    box-shadow: none;
    border-color: transparent;
}

.kmila-fpga-picker__list {
    overflow-y: auto;
    padding: var(--space-2);
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.kmila-fpga-picker__option {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    width: 100%;
    padding: var(--space-2) var(--space-3);
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--text-default);
    font-size: var(--fs-sm);
    font-family: var(--font-mono);
    text-align: left;
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-standard);
}

.kmila-fpga-picker__option > span {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.kmila-fpga-picker__option:hover {
    background-color: var(--surface-3);
    color: var(--text-strong);
}

.kmila-fpga-picker__option.is-selected {
    background-color: var(--kmila-accent-soft);
    color: var(--text-strong);
}

.kmila-fpga-picker__check {
    color: var(--kmila-accent);
}

.kmila-fpga-picker__empty {
    padding: var(--space-4);
    text-align: center;
    color: var(--text-muted);
    font-style: italic;
}


/* Responsive: shrink the FPGA summary on very narrow viewports */
@media (max-width: 575.98px) {
    .kmila-fpga-picker__summary { max-width: 160px; }
    .kmila-fpga-picker__family,
    .kmila-fpga-picker__chevron { display: none; }
}


/* ============================================================================
   Language picker (details popover — matches FPGA picker style)
   ============================================================================ */

.kmila-lang-picker {
    position: relative;
}

.kmila-lang-picker__summary {
    list-style: none;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    color: var(--text-default);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    user-select: none;
    transition: background-color var(--duration-fast) var(--ease-standard),
                border-color var(--duration-fast) var(--ease-standard);
}

.kmila-lang-picker__summary::-webkit-details-marker { display: none; }
.kmila-lang-picker__summary::marker { display: none; content: ""; }

.kmila-lang-picker__summary:hover {
    background-color: var(--surface-2);
    border-color: var(--border-default);
}

.kmila-lang-picker[open] .kmila-lang-picker__summary {
    border-color: var(--kmila-accent);
    box-shadow: var(--shadow-focus);
}

.kmila-lang-picker__code {
    font-family: var(--font-mono);
    letter-spacing: var(--tracking-wide);
    color: var(--text-strong);
}

.kmila-lang-picker__chevron {
    color: var(--text-muted);
    transition: transform var(--duration-fast) var(--ease-standard);
}

.kmila-lang-picker[open] .kmila-lang-picker__chevron {
    transform: rotate(180deg);
}

.kmila-lang-picker__panel {
    position: absolute;
    top: calc(100% + var(--space-2));
    right: 0;
    left: auto;
    z-index: var(--z-dropdown);
    min-width: 220px;
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    padding: var(--space-2);
    display: flex;
    flex-direction: column;
    gap: 2px;
    animation: kmilaPop var(--duration-fast) var(--ease-standard);
    /* 7 languages × ~44 px would run 300+ px tall; when the pill sits mid-page
       on a phone the last entries (JP, AR) end up off-screen and unreachable
       because the panel doesn't scroll internally (real-device audit
       2026-08-02, F-046). Cap to viewport minus safe-area and let it scroll. */
    max-height: min(70dvh, var(--kmila-viewport-h, 70dvh) - 32px);
    overflow-y: auto;
    overscroll-behavior: contain;
}

[dir="rtl"] .kmila-lang-picker__panel {
    right: auto;
    left: 0;
}

.kmila-lang-picker__option {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    padding: var(--space-2) var(--space-3);
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--text-default);
    font-size: var(--fs-sm);
    text-align: left;
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-standard);
}

.kmila-lang-picker__option:hover {
    background-color: var(--surface-3);
    color: var(--text-strong);
}

.kmila-lang-picker__option.is-selected {
    background-color: var(--kmila-accent-soft);
    color: var(--text-strong);
}

.kmila-lang-picker__badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 36px;
    padding: 2px 8px;
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    color: var(--text-muted);
    background-color: var(--surface-3);
    border-radius: var(--radius-xs);
    flex-shrink: 0;
}

.kmila-lang-picker__option.is-selected .kmila-lang-picker__badge {
    color: var(--kmila-accent);
    background-color: color-mix(in srgb, var(--kmila-accent) 14%, transparent);
}

.kmila-lang-picker__name {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.kmila-lang-picker__check {
    color: var(--kmila-accent);
    flex-shrink: 0;
}


/* ============================================================================
   Theme picker (details popover)
   ============================================================================ */

.kmila-theme-picker {
    position: relative;
}

.kmila-theme-picker__summary {
    list-style: none;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    width: 44px;
    height: 40px;
    padding: 0;
    background-color: var(--surface-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    color: var(--text-default);
    user-select: none;
    transition: background-color var(--duration-fast) var(--ease-standard),
                border-color var(--duration-fast) var(--ease-standard);
}

.kmila-theme-picker__summary::-webkit-details-marker { display: none; }
.kmila-theme-picker__summary::marker { display: none; content: ""; }

.kmila-theme-picker__summary:hover {
    background-color: var(--surface-2);
    border-color: var(--border-default);
}

.kmila-theme-picker[open] .kmila-theme-picker__summary {
    border-color: var(--kmila-accent);
    box-shadow: var(--shadow-focus);
}

/* The chevron is hidden on the compact square trigger to save space; only the
   active-theme icon is visible. */
.kmila-theme-picker__chevron {
    display: none;
}

.kmila-theme-picker__panel {
    position: absolute;
    top: calc(100% + var(--space-2));
    right: 0;
    left: auto;
    z-index: var(--z-dropdown);
    min-width: 180px;
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    padding: var(--space-2);
    display: flex;
    flex-direction: column;
    gap: 2px;
    animation: kmilaPop var(--duration-fast) var(--ease-standard);
}

[dir="rtl"] .kmila-theme-picker__panel {
    right: auto;
    left: 0;
}

.kmila-theme-picker__option {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    padding: var(--space-2) var(--space-3);
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--text-default);
    font-size: var(--fs-sm);
    text-align: left;
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-standard);
}

.kmila-theme-picker__option > span {
    flex: 1 1 auto;
    font-weight: var(--fw-medium);
}

.kmila-theme-picker__option:hover {
    background-color: var(--surface-3);
    color: var(--text-strong);
}

.kmila-theme-picker__option.is-selected {
    background-color: var(--kmila-accent-soft);
    color: var(--text-strong);
}

.kmila-theme-picker__option.is-selected .bi {
    color: var(--kmila-accent);
}

.kmila-theme-picker__check {
    color: var(--kmila-accent);
    flex-shrink: 0;
}


/* ============================================================================
   Projects list (/projects)
   ============================================================================ */

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

.projects-header {
    display: grid;
    /* `minmax(0, auto)` on the tools column lets it shrink below the
       intrinsic min-content (the sum of all buttons' natural widths) when
       the page is narrower than the toolbar's natural size. Without `0` as
       the lower bound, the auto column refuses to shrink and pushes the
       rightmost button past the viewport, clipping "Crear nuevo proyecto"
       to "Crear nuevo" at tablet landscape widths. Combined with
       `flex-wrap: wrap` on .projects-header__tools, buttons wrap to a
       second row instead of disappearing. */
    grid-template-columns: 1fr minmax(0, auto);
    align-items: end;
    gap: var(--space-3);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
}

.projects-header__titles {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.projects-header__title {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    margin: 0;
    font-size: var(--fs-2xl);
    letter-spacing: var(--tracking-tight);
    color: var(--text-strong);
}

.projects-header__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 32px;
    height: 28px;
    padding: 0 10px;
    background-color: var(--surface-3);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    border-radius: var(--radius-pill);
}

.projects-header__tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-2);
}


/* Search input */
.projects-search {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    width: clamp(200px, 34vw, 360px);
    padding: 0 var(--space-3);
    background-color: var(--surface-2);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-pill);
    color: var(--text-muted);
    transition: border-color var(--duration-fast) var(--ease-standard),
                box-shadow var(--duration-fast) var(--ease-standard);
}

.projects-search:focus-within {
    border-color: var(--kmila-accent);
    box-shadow: var(--shadow-focus);
}

.projects-search .form-control {
    flex: 1 1 auto;
    border: none;
    background: transparent;
    padding: var(--space-2) 0;
    box-shadow: none;
    color: var(--text-default);
}

.projects-search .form-control:focus {
    box-shadow: none;
    border-color: transparent;
}

.projects-search__clear {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    background-color: var(--surface-3);
    color: var(--text-muted);
    border: none;
    border-radius: var(--radius-pill);
    cursor: pointer;
    font-size: var(--fs-xs);
    transition: background-color var(--duration-fast) var(--ease-standard),
                color var(--duration-fast) var(--ease-standard);
}

.projects-search__clear:hover {
    background-color: var(--danger-soft);
    color: var(--danger);
}


/* Primary CTA button */
.projects-cta {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    white-space: nowrap;
    box-shadow: var(--shadow-1);
}


/* Card grid */
.projects-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: var(--space-4);
}


/* Project card */
.project-card {
    position: relative;
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    overflow: hidden;
    transition: box-shadow var(--duration-base) var(--ease-standard),
                border-color var(--duration-base) var(--ease-standard),
                transform var(--duration-base) var(--ease-standard);
}

.project-card:hover {
    box-shadow: var(--shadow-2);
    border-color: var(--border-default);
    transform: translateY(-2px);
}

.project-card:focus-within {
    border-color: var(--kmila-accent);
    box-shadow: var(--shadow-focus);
}

.project-card__link {
    display: grid;
    grid-template-areas:
        "avatar body"
        "meta meta";
    grid-template-columns: auto 1fr;
    gap: var(--space-3);
    padding: var(--space-4);
    color: inherit;
    text-decoration: none;
}

.project-card__link:hover {
    color: inherit;
    text-decoration: none;
}

.project-card__avatar {
    grid-area: avatar;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    background-color: var(--kmila-accent-soft);
    color: var(--kmila-accent);
    border-radius: var(--radius-md);
    font-size: var(--fs-lg);
    flex-shrink: 0;
}

.project-card__body {
    grid-area: body;
    min-width: 0;
}

.project-card__name {
    margin: 0 0 var(--space-1);
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    line-height: var(--lh-snug);
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 1;
    line-clamp: 1;
    -webkit-box-orient: vertical;
}

.project-card__description {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--fs-sm);
    line-height: var(--lh-snug);
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    min-height: calc(var(--lh-snug) * var(--fs-sm) * 2);
}

.project-card__description--muted {
    font-style: italic;
    color: var(--text-subtle);
}

.project-card__meta {
    grid-area: meta;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding-top: var(--space-3);
    border-top: 1px solid var(--border-subtle);
    color: var(--text-muted);
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
}

.project-card__date {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}


/* Action rail — absolute top-right, revealed on hover (always on touch) */
.project-card__actions {
    position: absolute;
    top: var(--space-3);
    right: var(--space-3);
    display: flex;
    gap: 2px;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--duration-fast) var(--ease-standard);
}

[dir="rtl"] .project-card__actions {
    right: auto;
    left: var(--space-3);
}

.project-card:hover .project-card__actions,
.project-card:focus-within .project-card__actions {
    opacity: 1;
    pointer-events: auto;
}

@media (hover: none) {
    /* Touch devices — always show actions; hover transitions don't fire reliably */
    .project-card__actions {
        opacity: 1;
        pointer-events: auto;
    }
}

.project-card__action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border: 1px solid var(--border-subtle);
    background-color: var(--surface-2);
    color: var(--text-muted);
    border-radius: var(--radius-sm);
    text-decoration: none;
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-standard),
                color var(--duration-fast) var(--ease-standard),
                border-color var(--duration-fast) var(--ease-standard);
}

.project-card__action:hover {
    background-color: var(--surface-3);
    color: var(--text-strong);
    text-decoration: none;
}

.project-card__action--danger:hover {
    background-color: var(--danger-soft);
    color: var(--danger);
    border-color: var(--danger);
}


/* --- Skeleton loading cards --- */

.project-card--skeleton {
    padding: var(--space-4);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.skeleton {
    background: linear-gradient(
        90deg,
        var(--surface-3) 0%,
        var(--surface-1) 50%,
        var(--surface-3) 100%
    );
    background-size: 200% 100%;
    animation: kmilaShimmer 1.6s linear infinite;
    border-radius: var(--radius-xs);
}

.skeleton--avatar { width: 44px; height: 44px; border-radius: var(--radius-md); }
.skeleton--title  { height: 16px; width: 65%; }
.skeleton--line   { height: 12px; width: 100%; }
.skeleton--short  { width: 40%; }

@keyframes kmilaShimmer {
    from { background-position: 200% 0; }
    to   { background-position: -200% 0; }
}


/* --- Empty states --- */

.projects-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    padding: var(--space-8) var(--space-5);
    background-color: var(--surface-2);
    border: 1px dashed var(--border-default);
    border-radius: var(--radius-xl);
    text-align: center;
}

.projects-empty--compact {
    padding: var(--space-6) var(--space-4);
}

.projects-empty__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 72px;
    height: 72px;
    background-color: var(--kmila-accent-soft);
    color: var(--kmila-accent);
    border-radius: var(--radius-pill);
    font-size: var(--fs-xl);
    margin-bottom: var(--space-2);
}

.projects-empty h2 {
    margin: 0;
    font-size: var(--fs-lg);
    color: var(--text-strong);
}

.projects-empty p {
    margin: 0;
    max-width: 48ch;
    color: var(--text-muted);
    line-height: var(--lh-relaxed);
}

.projects-empty .btn {
    margin-top: var(--space-2);
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}

/* Header "Try a sample" — ghost button, picks up accent tint on hover */
.projects-sample {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    white-space: nowrap;
    color: var(--kmila-accent);
}

.projects-sample:hover {
    background-color: var(--kmila-accent-soft);
    color: var(--kmila-accent);
}


/* ============================================================================
   Settings page (/settings)
   ============================================================================ */

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

.settings-page__header {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
}

.settings-page__header h1 {
    margin: 0;
    font-size: var(--fs-2xl);
    letter-spacing: var(--tracking-tight);
    color: var(--text-strong);
}

.settings-page__intro {
    margin: 0;
    color: var(--text-muted);
    line-height: var(--lh-relaxed);
}

.settings-page__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
    /* Wider gutter so each card reads as its own object rather than a
       tile in a continuous wall. Backlog #6: "no clear separation". */
    gap: var(--space-5);
}

/* F-032 (real-device audit 2026-08-02) — Settings → Text-size row wrapped
   "Compact · Small · Normal · Comfortable · Large" to two lines on phones
   with `Large` alone on line 2. Make the row a proper flex line that
   scrolls horizontally below 480 CSS px instead of wrapping, matching
   the dock-tabs / view-tabs affordance elsewhere. */
.kmila-text-size-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    align-items: center;
}
@media (max-width: 480px) {
    .kmila-text-size-row {
        /* F-032b (Round 9 2026-08-05) — !important IS load-bearing. The base
           `.kmila-text-size-row { flex-wrap: wrap }` declared at line 1667
           has the same specificity as this media-scoped rule, and Chromium's
           computed style resolves to `wrap` even though this rule is later
           in the file (CDP-verified: with `!important` removed, the row
           shows `computed flex-wrap: wrap`). The specific cascade path is
           unclear but the behavior is deterministic. Retain `!important`.
           If you need to remove it, first change the base rule at line 1667
           to `nowrap` and drop this override — that's the cleanest path. */
        flex-wrap: nowrap !important;
        overflow-x: auto !important;
        scrollbar-width: none;
        -webkit-mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
                mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
    }
    .kmila-text-size-row::-webkit-scrollbar { display: none; }
    .kmila-text-size-row .kmila-btn-tab {
        flex: 0 0 auto !important;
        padding-inline: var(--space-2);
    }
}


.settings-card {
    background-color: var(--surface-2);
    /* Stronger boundary so cards visually separate from the page bg
       and from each other. Backlog #6. */
    border: 1px solid var(--border-default);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    /* Establish an inline-size container so every .settings-row can decide,
       via the @container (max-width: 520px) rule below, whether to sit
       label-beside-control or stack them. Cards park at ~360-370px in the
       2/3-up grid — well under 520px — so without this the query never fires
       and the 320px control column crushes the label to one-word-per-line.
       min-width:0 lets a long unbreakable localized word wrap instead of
       bumping the card past its grid track. */
    container-type: inline-size;
    min-width: 0;
}

/* When any <details> popover inside the card is open (currently the
   LanguageChanger, potentially future pickers), lift the overflow clip
   so the absolutely-positioned panel can extend below the card boundary.
   Otherwise 7 languages × ~44 px ran below the card's bottom edge and
   the last entries (JP, AR) became physically unreachable on phones
   (real-device audit 2026-08-02, F-046). The German-compound-noun
   overflow guard the base rule provides is only needed when the card is
   idle — once details[open] is set, the popover is the priority. */
.settings-card:has(details[open]) {
    /* Lift the overflow clip so an open popover (LanguageChanger's 7-language
       list) can extend below the card boundary instead of being cut off.
       container-type / min-width now live on the base .settings-card rule
       so the row stacking query fires for every card, not only open ones. */
    overflow: visible;
}

.settings-card__head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-3) var(--space-2);
    /* Backlog #6 retry: head no longer reads as a banner-on-card. Dropped
       the separate background-color and the border-bottom — head and body
       now share the card's surface so each card reads as one cohesive
       block rather than "banner + sub-card". Outer card boundary (border,
       shadow, gap) carries the separation between cards. */
}

.settings-card__head h2 {
    margin: 0 0 2px;
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
}

.settings-card__sub {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--fs-sm);
    line-height: var(--lh-snug);
    /* Cap reading width so the subtitle doesn't run as a single line
       on wide --wide cards, where labels look stretched and unbalanced
       next to the icon. */
    max-width: 60ch;
}

.settings-card__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Tightened from 36px so the icon stops dominating the head's
       vertical budget. Backlog #6 retry. */
    width: 28px;
    height: 28px;
    border-radius: var(--radius-sm);
    font-size: var(--fs-sm);
    flex-shrink: 0;
}

.settings-card__icon--accent  { background-color: var(--kmila-accent-soft); color: var(--kmila-accent); }
.settings-card__icon--success { background-color: var(--success-soft); color: var(--success); }
.settings-card__icon--info    { background-color: var(--info-soft); color: var(--kmila-accent); }
.settings-card__icon--warning { background-color: var(--warning-soft); color: var(--warning); }

.settings-card__body {
    /* Tightened from var(--space-3) var(--space-4). User feedback was that
       the cards looked padding-heavy across desktop + mobile; aligning to
       the head's smaller padding makes the rows feel like a continuation
       of the header rather than a separate padded sub-card. */
    padding: var(--space-2) var(--space-3);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

/* The single canonical actions row. Used by every card that has a
   button strip (Feedback, Bundle, FPGA, Languages). Keeps `align-items:
   center` so the InputFile-as-label button aligns with sibling buttons
   even when their internal text heights differ slightly. */
.settings-card__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    /* Bound width so a wide card doesn't stretch the actions row into
       a horizon-line of buttons. */
    min-width: 0;
}

/* Defensive: lets long-text buttons / wide selects shrink to fit
   their slot in the row instead of forcing the row to overflow the
   card horizontally. Without min-width: 0 a flex child clamps to its
   intrinsic min-content (the longest unbreakable word inside) and a
   neighbor button gets pushed off-screen. */
.settings-card__actions > * {
    min-width: 0;
    max-width: 100%;
}

.settings-card__hint {
    margin: 0;
    font-size: var(--fs-xs);
    color: var(--text-muted);
    /* Same readability cap as .settings-card__sub. */
    max-width: 60ch;
}

.settings-card__placeholder {
    margin: 0;
    padding: var(--space-3);
    color: var(--text-muted);
    font-size: var(--fs-sm);
    font-style: italic;
    text-align: center;
    border: 1px dashed var(--border-default);
    border-radius: var(--radius-sm);
    background-color: var(--surface-1);
}

/* Direct-child only — without the `>` this rule used to bleed into buttons
   nested inside .settings-row__control, jamming them flex-start on the row's
   right column instead of letting the row grid place them. */
.settings-card__body > .btn {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}

/* Inner text container of every .settings-card__head — title + sub. Without
   `min-width: 0` long localized titles can refuse to wrap inside their grid
   cell; `flex: 1` lets the block consume the remaining row space next to the
   icon so the sub paragraph lays out predictably. */
.settings-card__head-text {
    min-width: 0;
    flex: 1 1 auto;
}

/* Form-control cap inside cards — applies to the bundle picker, the
   simulation tick-resolution select, and any future <select>/<input> sized
   to content. Replaces inline `style="max-width: …"` sprinkled in markup.
   360px (was 260) fits the longest-locale placeholder
   ("Selecciona un proyecto para exportar…" / "Wähle ein Projekt zum Exportieren…")
   without truncation at the closed-state display. */
.settings-card__select {
    max-width: 360px;
}


/* Row inside a card: label on the left, capped control on the right.
   `minmax(0, 320px)` keeps the control column from ballooning past 320 px
   even when the inner control would otherwise grow to fit (e.g. native
   <select> rendering long translated option text). The 0 lower bound lets
   the column shrink below its content's natural min, working around the
   default `auto` minimum. */
.settings-row {
    display: grid;
    grid-template-columns: 1fr minmax(0, 320px);
    align-items: center;
    gap: var(--space-4);
}

.settings-row__label {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.settings-row__label strong {
    color: var(--text-strong);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
}

.settings-row__label small {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    line-height: var(--lh-snug);
}

.settings-row__control {
    min-width: 0;
    justify-self: end;
    width: 100%;
}

/* Native <select>/<input> inside a row should fill the capped column so
   the control's right edge aligns with the card's right padding. */
.settings-row__control > .form-select,
.settings-row__control > .form-control,
.settings-row__control > select,
.settings-row__control > input {
    width: 100%;
}

/* Empty-state placeholder centred when it's the only thing in the body —
   keeps "Coming soon" cards visually balanced next to data-rich siblings
   in the same grid row instead of hugging the top. :has() is widely
   supported in modern Chromium / WebKit (Safari 15.4+, Chrome 105+) which
   covers the MAUI BlazorWebView and our Web target. */
.settings-card__body:has(> .settings-card__placeholder:only-child) {
    flex: 1 1 auto;
    justify-content: center;
}

/* Narrow-CARD stacking — a container query so each card decides
   layout based on its own width, independent of viewport.
   The Settings grid often parks cards at ~360 px even on a 1280 px
   desktop (2-up layout), where a viewport-based media query can't see
   that things are cramped. Below 520 container px the label gets
   overrun by the 320 px-capped control; stacking gives the label its
   full row width, which fixes both the "text wraps too aggressively"
   and "buttons overlap text" symptoms in the same pass.
*/
@container (max-width: 520px) {
    .settings-row {
        grid-template-columns: 1fr;
        gap: var(--space-2);
    }
    .settings-row__control {
        justify-self: stretch;
        max-width: none;
    }
}

/* Viewport fallback for very small screens (phone portrait) — a
   safety net in case container queries are unavailable on the host
   browser. Same shape as the @container rule above. */
@media (max-width: 520px) {
    .settings-row {
        grid-template-columns: 1fr;
        gap: var(--space-2);
    }
    .settings-row__control {
        justify-self: stretch;
        max-width: none;
    }
    /* (Card head/body padding is now tight by default — see the base
       .settings-card__head and .settings-card__body rules above. No
       per-viewport override needed any more.) */
}


/* ============================================================================
   Logs page (/logs)
   ============================================================================ */

.logs-page {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    height: calc(100dvh - var(--topbar-height) - 120px); /* viewport minus top + padding */
    min-height: 480px;
}

.logs-page__header {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
}

.logs-page__titles {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
}

.logs-page__title {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    margin: 0;
    font-size: var(--fs-2xl);
    letter-spacing: var(--tracking-tight);
    color: var(--text-strong);
}

.logs-page__count {
    display: inline-flex;
    align-items: center;
    padding: 2px 10px;
    background-color: var(--surface-3);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    border-radius: var(--radius-pill);
}

.logs-page__tools {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.logs-page__search {
    width: clamp(200px, 32vw, 320px);
}

.logs-page__level {
    /* Override Bootstrap's `form-select` default chrome with kmila tokens
       so the filter dropdown matches the surrounding surface (audit
       2026-06-14, finding A-11). Custom chevron is svg-encoded inline. */
    width: auto;
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    appearance: none;
    -webkit-appearance: none;
    padding: var(--space-1) calc(var(--space-3) + 18px) var(--space-1) var(--space-3);
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    color: var(--text-default);
    background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='currentColor'%3e%3cpath d='M3.204 5L8 10.481 12.796 5z'/%3e%3c/svg%3e");
    background-repeat: no-repeat;
    background-position: right var(--space-2) center;
    background-size: 12px;
    transition: border-color var(--duration-fast) var(--ease-standard);
}
.logs-page__level:hover { border-color: var(--border-default); }
.logs-page__level:focus-visible {
    outline: none;
    border-color: var(--kmila-accent);
    box-shadow: var(--shadow-focus);
}
[dir="rtl"] .logs-page__level {
    padding-inline: var(--space-3) calc(var(--space-3) + 18px);
    background-position: left var(--space-2) center;
}

.logs-page__toggle {
    padding: 6px 8px;
}

.logs-page__toggle.is-active {
    background-color: var(--kmila-accent-soft);
    color: var(--kmila-accent);
}


/* List reuses the Output row styles from the dock for visual consistency */
.logs-page__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
    background-color: var(--border-subtle);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0;
}

.logs-page__list .output-row {
    grid-template-columns: auto auto 1fr auto;
    align-items: start;
}

.logs-row__report {
    align-self: center;
    flex-shrink: 0;
    color: var(--text-muted);
}

.logs-row__report:hover {
    color: var(--danger);
}

.logs-row__body {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.logs-row__category {
    /* Source identifier (already PascalCase from ShortenCategory). Render in
       plain mono — uppercase + tracking-wide read as a column heading and
       collided with the same identifier echoed inside the message body. */
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-regular);
    color: var(--text-muted);
}

.logs-page__untranslated-note {
    display: block;
    margin-top: var(--space-1);
    font-size: var(--fs-xs);
    color: var(--text-muted);
    font-style: italic;
}

.logs-row__exception {
    margin: 4px 0 0;
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-xs);
    font-size: var(--fs-xs);
    white-space: pre-wrap;
    word-break: break-word;
    color: var(--danger);
    max-height: 160px;
    overflow-y: auto;
}

.logs-page__empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-7) var(--space-4);
    text-align: center;
    border: 1px dashed var(--border-default);
    border-radius: var(--radius-md);
    color: var(--text-muted);
}

.logs-page__empty h2 {
    margin: 0;
    font-size: var(--fs-lg);
    color: var(--text-strong);
}

.logs-page__empty p {
    margin: 0;
    max-width: 48ch;
    line-height: var(--lh-relaxed);
}


/* Responsive */
@media (max-width: 640.98px) {
    .projects-header {
        grid-template-columns: 1fr;
    }
    .projects-header__tools {
        width: 100%;
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-2);
    }
    .projects-search { width: 100%; }
    .projects-cta { width: 100%; justify-content: center; }
    /* F-011 (Projects action-row asymmetry, R9 2026-08-05) — the DOM order
       [Sample, Examples, Create new] rendered as `[Sample | Examples]` on line
       1 and `[Create new]` full-width on line 2 (Round 8 baseline), which read
       as visually off-balance (2 up top, 1 below). Lift the primary CTA to
       order:-1 so the visible order becomes `[Create new] · [Sample | Examples]`
       — main action full-width on top with a clear hierarchy, secondaries
       symmetric on the row below (each takes half via flex-grow). */
    .projects-cta { order: -1; }
    .projects-sample,
    .projects-examples {
        flex: 1 1 0;
        min-width: 0;
        justify-content: center;
    }
    .project-card__actions { opacity: 1; pointer-events: auto; }
}


/* ============================================================================
   Project form (create modal body)
   ============================================================================ */

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

.project-form__field {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.project-form__label {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    margin: 0;
}

.project-form__required {
    color: var(--danger);
    font-weight: var(--fw-bold);
}

.project-form__optional {
    font-size: var(--fs-xs);
    font-weight: var(--fw-regular);
    color: var(--text-subtle);
    text-transform: lowercase;
    letter-spacing: 0;
}

.project-form__hint {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--text-muted);
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    word-break: break-all;
}

.project-form__hint .bi {
    color: var(--kmila-accent);
    flex-shrink: 0;
}

.project-form__validation {
    color: var(--danger);
    font-size: var(--fs-xs);
}

.project-form__actions {
    display: flex;
    justify-content: flex-end;
    margin-top: var(--space-3);
}

.project-form__submit {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding-left: var(--space-5);
    padding-right: var(--space-5);
}

.project-form__submit:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}


/* ============================================================================
   Editor (/editor/{id})
   ============================================================================ */

.editor-page {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
    /* Same flex-shrink guard as the dock below — keeps the page from ever
       reporting a larger intrinsic width than its container. */
    min-width: 0;
    /* B-3 (editor-mobile audit 2026-06-29): flex-fill the available height
       inside `<article class="content">` so descendants like the editor
       workbench and the block canvas have a real height context to grow
       into. Without this the page sized itself to content and the canvas
       left a 400+ px empty band above the dock on phones. */
    flex: 1 1 auto;
    min-height: 0;
}

/* B-3 (editor-mobile audit 2026-06-29): make the route article a flex
   column when it hosts the editor, so .editor-page can actually flex-fill
   its parent. The MainLayout's `<article class="content">` defaults to
   block layout — `flex: 1 0 auto` makes IT fill the shell, but children
   inside still position normally. Switching to flex column propagates the
   height context to .editor-page.
   Scoped to phone (≤ 768 px) so the desktop's content-height behaviour
   for the editor card stays unchanged. */
@media (max-width: 768px) {
    /* R3-3 (editor-mobile audit 2026-07-02): scoped outer-scroll lock
       for the editor route. During Monaco / Visual / Blocks mount the
       flex chain briefly overflows its box; without a guardrail on the
       shell + content, that transient overflow opens a second (outer)
       scrollbar that steals swipe gestures. `.editor-workbench` already
       clips at `overflow: hidden`, and Monaco / Blocks canvas / Visual
       canvas own their inner `overflow: auto`, so the inner scroll
       survives. Non-editor routes are untouched thanks to `:has(.editor-page)`. */
    .kmila-shell__main:has(.editor-page) {
        overflow: hidden;
    }
    .kmila-shell__main > .content:has(.editor-page) {
        display: flex;
        flex-direction: column;
        min-height: 0;
        overflow: hidden;
    }
}


/* --- Workbench: header + body with right rail --- */

.editor-workbench {
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-1);
    overflow: hidden;
    /* B-3 (editor-mobile audit 2026-06-29): the workbench was sized by
       content with no flex context of its own, so child views like the
       Blocks canvas — even with `flex: 1 1 auto` on every descendant —
       had no parent height to fill, leaving a big empty band between the
       canvas and the dock. Establish the flex column so descendants can
       claim the editor card's full vertical budget. */
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

.editor-workbench__header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-4);
    background-color: var(--surface-1);
    border-bottom: 1px solid var(--border-subtle);
    min-height: 44px;
    /* Same flex-shrink guard as the tab strip above: Monaco's min-height
       was eating this row when a file was open, leaving only the code visible
       without the file name / Ln·Col indicator. */
    flex-shrink: 0;
}

/* On phones the workbench header (file name + lang chip on the left,
   Ln/Col + split toggle + Ctrl+S kbd hint on the right) duplicates the
   file-tab strip below it and eats ~44 px of already-scarce vertical
   space. Split-panes and Ctrl+S both belong to desktop, and the tab
   strip already carries the file name — so hide the whole header on
   ≤ 768 px. Real-device audit 2026-08-02 (F-025). */
@media (max-width: 768px) {
    .editor-workbench__header { display: none; }
}

.editor-file-info {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    flex: 1 1 auto;
}

.editor-file-icon {
    color: var(--kmila-accent);
    font-size: var(--fs-base);
    flex-shrink: 0;
}

.editor-file-name {
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    color: var(--text-strong);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}

/* Empty state: when no file is open the name slot shows a localized
   placeholder ("No file open") and the language chip is hidden — the
   placeholder is descriptive prose, not a filename, so it shouldn't be
   monospaced and shouldn't be paired with a redundant VHDL badge. */
.editor-file-name--empty {
    font-family: var(--font-sans);
    color: var(--text-muted);
    font-weight: var(--fw-regular);
}

.editor-unsaved {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background-color: var(--warning);
    box-shadow: 0 0 0 4px var(--warning-soft);
    flex-shrink: 0;
}

.editor-lang-chip {
    display: inline-flex;
    align-items: center;
    padding: 2px 8px;
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--kmila-accent);
    background-color: var(--kmila-accent-soft);
    border-radius: var(--radius-pill);
    flex-shrink: 0;
}


/* --- Status / shortcut hint on the right of the header --- */

.editor-status {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    flex-shrink: 0;
    color: var(--text-muted);
    font-size: var(--fs-xs);
}

.editor-position {
    font-family: var(--font-mono);
    white-space: nowrap;
}

.editor-shortcut {
    display: inline-flex;
    align-items: center;
    gap: 2px;
}

.editor-shortcut kbd {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.6em;
    padding: 1px 5px;
    background-color: var(--surface-2);
    border: 1px solid var(--border-default);
    border-bottom-width: 2px;
    border-radius: var(--radius-xs);
    color: var(--text-default);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    line-height: 1;
}

@media (max-width: 575.98px) {
    .editor-shortcut { display: none; }
}


/* --- Body: main editor area + right rail --- */

.editor-workbench__body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 260px;
    gap: 1px;                      /* hairline gap = a subtle divider via body bg */
    background-color: var(--border-subtle);
}

.editor-workbench__main {
    min-width: 0;
    background-color: var(--surface-2);
}

.editor-workbench__rail {
    background-color: var(--surface-1);
    padding: var(--space-3);
}

/* Strip BB card chrome inside the rail so ButtonCollapse blends in */
.editor-workbench__rail .card {
    background-color: transparent;
    border: none;
    box-shadow: none;
}

.editor-workbench__rail .card-body {
    padding: 0;
}

.editor-workbench__rail .card-title {
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
    font-family: var(--font-mono);
    margin-bottom: var(--space-3);
}


/* Smart-hide the Monaco minimap on narrow desktops and mobile — it takes
   ~160px of horizontal width that matters more on small viewports, and it's
   not tappable anyway on touch so there's nothing lost by hiding it. */
@media (max-width: 900px) {
    .monaco-editor .minimap { display: none !important; }
}


/* --- Monaco editor sizing --- */

.editor-monaco,
.monaco-editor-container,
#editor {
    width: 100%;
    height: 60dvh;
    min-height: 400px;
    /* F-078 (R9 2026-08-05, CDP-verified critical bug) — under `<html dir="rtl">`
       Monaco inherits `direction: rtl`, which cascades into `.view-line`
       elements. Monaco's internal viewport math assumes LTR and computes each
       line's `left` as an offset from the container's inline-start. In RTL
       mode inline-start = right edge, so those "left offsets" are added to a
       massively-far-right origin, producing `.view-line { left: 12782469px }`
       — 12 million pixels off-screen. The Monaco editor appears completely
       blank on Arabic. Force `direction: ltr` on the container so Monaco's
       LTR-assumed layout works. Code is monospace ASCII anyway — a code
       editor's inline axis has nothing to do with the app-wide language
       direction. Editor UI elements (gutter, minimap, scrollbar) naturally
       belong on the LTR-native sides too. */
    direction: ltr;
    text-align: left;
}

/* F-079 (R9 2026-08-05, sibling of F-078) — the same RTL-inheritance trap
   hits every viewer that renders LTR-native content (code blocks, gate
   schematics, waveform traces, flow diagrams). Their internal layout
   assumes LTR and computes child positions with `left` offsets from an
   assumed left-edge origin; under `direction: rtl` those offsets are
   applied from the RIGHT edge and content lands far off-screen. Force
   LTR on each viewer's outer container so the app's Arabic UI can wrap
   an LTR content plane. Chrome (drawer, TopBar, breadcrumbs) still
   inherits RTL from `<html>` and mirrors correctly. */
.kmila-block-editor,
.schematic-viewer,
.waveform-viewer,
.flow-diagram-viewer,
/* F-091 (R10 2026-08-08, sibling of F-079) — /visual/{id} standalone.
   Same class of bug: kviz-widget children carry absolute `left: <px>` from
   layout code that assumes LTR. Under inherited RTL the palette / workspace
   / widget canvas renders with negative x-coordinates (some widgets land
   at left: -450 CSS px) and 227 CSS px of horizontal overflow scroll. Force
   LTR on the standalone Visual container. TopBar / breadcrumb / footer
   still inherit RTL from <html>. */
.visual-page {
    direction: ltr;
}

@media (max-width: 768px) {
    .editor-monaco,
    .monaco-editor-container,
    #editor {
        height: 55dvh;
        min-height: 320px;
    }
}

@media (max-width: 768px) and (orientation: landscape) {
    .editor-monaco,
    .monaco-editor-container,
    #editor {
        height: 50dvh;
        min-height: 240px;
    }
}


/* --- Markdown split view --- */

.editor-split {
    display: grid;
    grid-template-columns: 1fr 1fr;
    height: 60dvh;
    min-height: 400px;
}

.editor-split__pane {
    display: flex;
    flex-direction: column;
    min-width: 0;
    overflow: hidden;
    background-color: var(--surface-2);
}

.editor-split__pane + .editor-split__pane {
    border-left: 1px solid var(--border-subtle);
}

.editor-split__header {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-1);
    border-bottom: 1px solid var(--border-subtle);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
    font-family: var(--font-mono);
    min-height: 36px;
    flex-shrink: 0;
}

.editor-split__pane .editor-monaco {
    flex: 1 1 auto;
    min-height: 0;
    height: auto;
}

.editor-markdown {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: var(--space-5);
    color: var(--text-default);
    line-height: var(--lh-relaxed);
}

.editor-markdown h1,
.editor-markdown h2,
.editor-markdown h3,
.editor-markdown h4,
.editor-markdown h5,
.editor-markdown h6 { color: var(--text-strong); }

.editor-markdown a { color: var(--kmila-accent); }

.editor-markdown code {
    background-color: var(--surface-3);
    border: 1px solid var(--border-subtle);
    padding: 0.15em 0.4em;
    border-radius: var(--radius-xs);
    font-size: 0.9em;
    color: var(--text-strong);
}

.editor-markdown pre {
    background-color: var(--surface-3);
    border: 1px solid var(--border-subtle);
    padding: var(--space-4);
    border-radius: var(--radius-md);
    overflow-x: auto;
}

.editor-markdown pre code {
    background: transparent;
    border: none;
    padding: 0;
}

.editor-markdown blockquote {
    border-left: 3px solid var(--kmila-accent);
    padding: var(--space-1) var(--space-4);
    margin: var(--space-3) 0;
    color: var(--text-muted);
    background-color: var(--surface-1);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}

.editor-markdown table {
    width: 100%;
    border-collapse: collapse;
    margin: var(--space-3) 0;
}

.editor-markdown th,
.editor-markdown td {
    border: 1px solid var(--border-subtle);
    padding: var(--space-2) var(--space-3);
}

.editor-markdown th {
    background-color: var(--surface-1);
    color: var(--text-strong);
    font-weight: var(--fw-semibold);
}


/* --- Bottom dock: waveforms / entities / files panels --- */

.editor-panel {
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    overflow: hidden;
    box-shadow: var(--shadow-1);
}

.editor-panel__header {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border-subtle);
    background-color: var(--surface-1);
}

.editor-panel__header h2 {
    margin: 0;
    font-size: var(--fs-md);
    color: var(--text-strong);
}

.editor-panel__body {
    padding: var(--space-4);
}

.editor-panel__body--flush { padding: 0; }

/* Neutralise nested BB cards inside panels so the panel chrome is the single
   visual container. Nested cards inside EntityView / FilesCard would otherwise
   render card-in-card. */
.editor-panel .card {
    background-color: transparent;
    border: none;
    box-shadow: none;
}
.editor-panel .card > .card-body { padding: 0; }


/* Two-column grid for entities + files */
.editor-info-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: var(--space-4);
}


/* --- Responsive --- */

@media (max-width: 991.98px) {
    .editor-workbench__body {
        grid-template-columns: 1fr;
    }
    .editor-workbench__rail {
        border-top: 1px solid var(--border-subtle);
    }
}

@media (max-width: 768px) {
    .editor-split {
        grid-template-columns: 1fr;
        height: auto;
        min-height: 0;
    }
    .editor-split__pane + .editor-split__pane {
        border-left: none;
        border-top: 1px solid var(--border-subtle);
    }
    .editor-split__pane .editor-monaco { min-height: 280px; }
    .editor-markdown { min-height: 280px; max-height: 40dvh; }
}


/* ============================================================================
   Simulation Parameters Modal
   ============================================================================ */

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


/* --- Resource status strip --- */

.kmila-simulation__status {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    font-size: var(--fs-sm);
}

.kmila-simulation__status > .bi {
    font-size: var(--fs-lg);
    flex-shrink: 0;
}

.kmila-simulation__status-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    flex: 1 1 auto;
}

.kmila-simulation__status-title {
    font-weight: var(--fw-semibold);
}

.kmila-simulation__status-fpga {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.kmila-simulation__status--success {
    background-color: var(--success-soft);
    border-color: var(--success);
    color: var(--success);
}

.kmila-simulation__status--warning {
    background-color: var(--warning-soft);
    border-color: var(--warning);
    color: var(--warning);
}

.kmila-simulation__status--danger {
    background-color: var(--danger-soft);
    border-color: var(--danger);
    color: var(--danger);
}

/* The coloured title should be the only accent; reset the FPGA line's color
   so the device name sits in the normal text color and stays readable. */
.kmila-simulation__status .kmila-simulation__status-fpga {
    color: inherit;
    opacity: 0.8;
}


/* --- Tab strip as segmented-pill control --- */

.kmila-simulation__tabs {
    margin: 0;
}

.kmila-simulation__tabs .nav-pills {
    display: inline-flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    padding: var(--space-1);
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    margin-bottom: var(--space-4);
}

.kmila-simulation__tabs .nav-item {
    margin: 0;
}

.kmila-simulation__tabs .nav-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    background-color: transparent;
    border: none;
    border-radius: var(--radius-pill);
    color: var(--text-muted);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    transition: background-color var(--duration-fast) var(--ease-standard),
                color var(--duration-fast) var(--ease-standard);
}

.kmila-simulation__tabs .nav-link:hover {
    background-color: var(--surface-2);
    color: var(--text-default);
}

.kmila-simulation__tabs .nav-link.active {
    background-color: var(--surface-2);
    color: var(--text-strong);
    box-shadow: var(--shadow-1);
}

.kmila-simulation__tabs .nav-link .bi {
    color: var(--text-muted);
    transition: color var(--duration-fast) var(--ease-standard);
}

.kmila-simulation__tabs .nav-link.active .bi {
    color: var(--kmila-accent);
}

.kmila-simulation__tab {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    position: relative;
}

.kmila-simulation__tab-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background-color: var(--warning);
    box-shadow: 0 0 0 3px var(--warning-soft);
}

/* Pulsing "LIVE" dot on the Run tab while simulation is running */
.kmila-simulation__tab-live {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background-color: var(--danger);
    box-shadow: 0 0 0 3px var(--danger-soft);
    animation: kmilaPulse 1.3s var(--ease-standard) infinite;
}

@keyframes kmilaPulse {
    0%, 100% { transform: scale(1); opacity: 1; }
    50%      { transform: scale(1.4); opacity: 0.6; }
}


/* Tab panel body */
.kmila-simulation__tabs .tab-content {
    padding: 0;
}

.kmila-simulation__tabs .tab-pane {
    padding: 0;
}

/* Strip any BB .card wrappers inside tab contents so we don't get card-in-card
   inside a modal that already has its own chrome. */
.kmila-simulation .card {
    background-color: transparent;
    box-shadow: none;
}

/* Bring back just a subtle border on inner cards (so port variables read as
   distinct groups without looking nested) */
.kmila-simulation .tab-content .card {
    border: 1px solid var(--border-subtle);
    background-color: var(--surface-2);
}


/* --- Responsive --- */

@media (max-width: 575.98px) {
    .kmila-simulation__tabs .nav-pills {
        display: flex;
        width: 100%;
        border-radius: var(--radius-md);
    }
    .kmila-simulation__tabs .nav-item { flex: 1 1 0; }
    .kmila-simulation__tabs .nav-link {
        width: 100%;
        justify-content: center;
        padding-left: var(--space-2);
        padding-right: var(--space-2);
    }
    .kmila-simulation__status {
        flex-wrap: wrap;
    }
}


/* ============================================================================
   Resource view — metric cards, progress bars, and the usage wrapper
   ---------------------------------------------------------------------------
   Semantic variants map onto token colors. Each variant defines three vars:
   --mx-fg (text color), --mx-bg (icon chip background), --mx-track (progress
   fill). The CSS then references those from a single rule, so there's no
   per-variant copy-paste.
   ============================================================================ */

/* --- Usage wrapper (rendered inside EntityView) --- */

.resource-usage {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border-subtle);
}

.resource-usage__header {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.resource-usage__title {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
}

.resource-usage__title .bi {
    color: var(--kmila-accent);
}

.resource-usage__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: var(--space-3);
}

.resource-usage__note {
    display: flex;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    background-color: var(--info-soft);
    border: 1px solid var(--kmila-accent);
    border-radius: var(--radius-md);
    color: var(--text-default);
    font-size: var(--fs-sm);
    line-height: var(--lh-relaxed);
}

.resource-usage__note .bi {
    color: var(--kmila-accent);
    font-size: var(--fs-md);
    flex-shrink: 0;
    margin-top: 2px;
}

.resource-usage__note p {
    margin: 0;
    min-width: 0;
}

.resource-usage__note strong {
    color: var(--text-strong);
}


/* --- Metric card --- */

.resource-metric {
    --mx-fg: var(--kmila-accent);
    --mx-bg: var(--kmila-accent-soft);

    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-1);
    min-width: 0;
    transition: box-shadow var(--duration-base) var(--ease-standard),
                border-color var(--duration-base) var(--ease-standard),
                transform var(--duration-base) var(--ease-standard);
}

.resource-metric:hover {
    box-shadow: var(--shadow-2);
    border-color: var(--border-default);
    transform: translateY(-1px);
}

.resource-metric--success { --mx-fg: var(--success); --mx-bg: var(--success-soft); }
.resource-metric--info    { --mx-fg: var(--kmila-accent); --mx-bg: var(--kmila-accent-soft); }
.resource-metric--warning { --mx-fg: var(--warning); --mx-bg: var(--warning-soft); }
.resource-metric--danger  { --mx-fg: var(--danger);  --mx-bg: var(--danger-soft); }
.resource-metric--neutral { --mx-fg: var(--text-muted); --mx-bg: var(--surface-3); }


.resource-metric__head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}

.resource-metric__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    background-color: var(--mx-bg);
    color: var(--mx-fg);
    border-radius: var(--radius-sm);
    font-size: var(--fs-base);
    flex-shrink: 0;
}

.resource-metric__label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
}

.resource-metric__title {
    display: block;
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    font-family: var(--font-mono);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.resource-metric__accuracy {
    display: inline-flex;
    align-items: center;
    padding: 1px 8px;
    background-color: var(--surface-3);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    border-radius: var(--radius-pill);
    flex-shrink: 0;
}


.resource-metric__value {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    min-width: 0;
    overflow: hidden;
}

.resource-metric__number {
    font-family: var(--font-mono);
    font-size: var(--fs-xl);
    font-weight: var(--fw-bold);
    color: var(--mx-fg);
    line-height: 1;
    overflow-wrap: anywhere;
    word-break: break-word;
    min-width: 0;
}

.resource-metric__unit {
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    flex-shrink: 0;
}


.resource-metric__extra {
    min-width: 0;
}

/* Clock signal chips rendered via ChildContent on the Clock Domains card */
.resource-metric__chips {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: var(--space-2);
}

.resource-metric__chip {
    display: inline-flex;
    align-items: center;
    padding: 2px 8px;
    background-color: var(--surface-3);
    color: var(--text-default);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    border-radius: var(--radius-pill);
    border: 1px solid var(--border-subtle);
}


/* --- ResourceProgressBar --- */

.resource-bar {
    --bx-fg: var(--success);
    --bx-bg: var(--success-soft);

    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3) 0;
}

.resource-bar__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.resource-bar__title {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--text-strong);
    font-size: var(--fs-sm);
}

.resource-bar__title .bi {
    color: var(--text-muted);
}

.resource-bar__counts {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
}

.resource-bar__used {
    font-weight: var(--fw-bold);
    padding: 1px 8px;
    border-radius: var(--radius-xs);
    background-color: var(--surface-3);
    color: var(--text-strong);
}

.resource-bar__used--success { background-color: var(--success-soft); color: var(--success); }
.resource-bar__used--info    { background-color: var(--kmila-accent-soft); color: var(--kmila-accent); }
.resource-bar__used--warning { background-color: var(--warning-soft); color: var(--warning); }
.resource-bar__used--danger  { background-color: var(--danger-soft);  color: var(--danger); }

.resource-bar__sep {
    color: var(--text-subtle);
}

.resource-bar__available {
    color: var(--text-muted);
}

.resource-bar__percent {
    margin-left: var(--space-1);
    color: var(--text-muted);
    font-size: var(--fs-xs);
}

@media (max-width: 575.98px) {
    .resource-bar__percent { display: none; }
}


/* Progress track */

.resource-bar__track {
    position: relative;
    width: 100%;
    height: 8px;
    background-color: var(--surface-3);
    border-radius: var(--radius-pill);
    overflow: hidden;
}

.resource-bar__fill {
    display: block;
    height: 100%;
    border-radius: inherit;
    background-color: var(--success);
    transition: width var(--duration-slow) var(--ease-standard),
                background-color var(--duration-base) var(--ease-standard);
}

.resource-bar__fill--success { background-color: var(--success); }
.resource-bar__fill--info    { background-color: var(--kmila-accent); }
.resource-bar__fill--warning { background-color: var(--warning); }
.resource-bar__fill--danger  {
    background: repeating-linear-gradient(
        45deg,
        var(--danger) 0,
        var(--danger) 8px,
        color-mix(in srgb, var(--danger) 70%, transparent) 8px,
        color-mix(in srgb, var(--danger) 70%, transparent) 16px
    );
    animation: kmilaPulseStripe 1.6s linear infinite;
}

@keyframes kmilaPulseStripe {
    from { background-position: 0 0; }
    to   { background-position: 32px 0; }
}


/* ============================================================================
   Editor Dock — bottom panel with 5 tabs on the Editor page.
   Chunk 1: tabs + content area + empty states. Drag-to-resize and collapsed
   mode come in Chunk 2; Run bar + status bar come in Chunks 3–4.
   ============================================================================ */

.kmila-dock {
    display: flex;
    flex-direction: column;
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-1);
    overflow: hidden;
    /* `min-width: 0` keeps the dock from expanding past its parent when a
       child (like the Waveforms tab's very wide SVG grid) reports a large
       intrinsic size. Without this, the whole editor shifted off-screen at
       high zoom levels. */
    min-width: 0;
    min-height: 280px;
    /* Containing block for the absolutely-positioned collapse toggle (which
       used to sit inside the scrolling tab strip — moved out so it stays
       reachable when 8 tabs overflow horizontally on phones). */
    position: relative;
    transition: min-height var(--duration-base) var(--ease-standard);
}

/* When collapsed, shrink to just the tab strip. The panel below is already
   hidden via the `hidden` HTML attribute; this just removes the empty space. */
.kmila-dock--collapsed {
    min-height: 0;
}


/* --- Tab strip --- */

.kmila-dock__tabs {
    display: flex;
    flex-wrap: nowrap;
    /* Tabs and the collapse chevron share one horizontal strip. `flex-shrink: 0`
       keeps the strip's own height when the panel below grows with content
       (Entities / Files etc.) — without it the flex column compressed the
       strip and the tabs appeared "cut in half". */
    flex-shrink: 0;
    align-items: stretch;
    gap: 2px;
    padding: var(--space-2) var(--space-2) 0;
    background-color: var(--surface-1);
    border-bottom: 1px solid var(--border-subtle);
    overflow-x: auto;
    scrollbar-width: none;
    /* Right-edge fade hints there are more tabs beyond the viewport. The
       dock has 8 tabs (entities / schematic / flow / ports / waveforms /
       runs / files / output); only 6 fit on a 1080-wide phone, so Files +
       Output sit off-screen and students had no way to know. The mask
       fades the last ~32px of the strip when content overflows. */
    -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 32px), transparent 100%);
            mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 32px), transparent 100%);
}

.kmila-dock__tabs::-webkit-scrollbar {
    display: none;
}

/* Collapse toggle is now a sibling of .kmila-dock__tabs (was inside it, but
   sticky-right-0 broke when 8 tabs overflowed on iOS WKWebView). Pinning it
   absolutely to the top-right of .kmila-dock keeps it reachable on every
   viewport and frees the scrolling tab strip from sharing horizontal space
   with the chevron. */
.kmila-dock__collapse {
    position: absolute;
    top: var(--space-2);
    right: max(var(--space-2), env(safe-area-inset-right, 0px));
    z-index: 4;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    cursor: pointer;
    font-size: var(--fs-base);
    box-shadow: var(--shadow-1);
    transition: background-color var(--duration-fast) var(--ease-standard),
                color var(--duration-fast) var(--ease-standard);
}

.kmila-dock__collapse:hover {
    background-color: var(--surface-3);
    color: var(--text-strong);
}

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

[dir="rtl"] .kmila-dock__collapse {
    right: auto;
    left: max(var(--space-2), env(safe-area-inset-left, 0px));
}

/* Reserve room at the right edge of the tab strip so the rightmost tab
   doesn't slide under the absolutely-positioned collapse button. */
.kmila-dock__tabs {
    padding-right: calc(32px + var(--space-3));
}
[dir="rtl"] .kmila-dock__tabs {
    padding-right: var(--space-2);
    padding-left: calc(32px + var(--space-3));
}

/* Tab visuals (color, hover underline, is-active state, count badge)
   come from .kmila-btn-tab (buttons.css). This selector now only carries
   dock-specific layout: flex-shrink so the strip doesn't compress. */
.kmila-dock__tab { flex-shrink: 0; }

/* Count badge — same shape as .kmila-btn-tab__count but with the dock's
   sticky color rules. The tab's `.is-active` cascade in buttons.css
   tints the count to the accent automatically. */
.kmila-dock__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Scale with surrounding text so the badge shrinks at xs and grows at
       xl user-text-scale settings; previous fixed-px sizing made the badge
       feel tiny at xl and oversized at xs relative to its label. */
    min-width: 1.5em;
    height: 1.3em;
    padding: 0 0.4em;
    background-color: var(--surface-3);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    border-radius: var(--radius-pill);
    transition: background-color var(--duration-fast) var(--ease-standard),
                color var(--duration-fast) var(--ease-standard);
}
.kmila-dock__tab.is-active .kmila-dock__count {
    background-color: var(--kmila-accent-soft);
    color: var(--kmila-accent);
}


.kmila-dock__dot {
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background-color: var(--kmila-accent);
    box-shadow: 0 0 0 3px var(--kmila-accent-soft);
}


/* On narrow viewports the tab strip stacks icon + label vertically so the
   label remains readable while the per-tab footprint stays compact. Tabs
   still scroll horizontally if 8+ of them won't fit (see .kmila-dock__tabs
   overflow-x). Previously the labels disappeared entirely below 480 px,
   which left new users staring at unlabeled icons. */
@media (max-width: 640.98px) {
    .kmila-dock__tab {
        flex-direction: column;
        padding: var(--space-1) var(--space-2);
        gap: 2px;
        min-width: 64px;
    }
    .kmila-dock__tab-label {
        font-size: var(--fs-2xs);
        line-height: 1.1;
        white-space: nowrap;
    }
}
/* Narrower phones (≤ 480 px CSS) fall back to icon-only so all 8 tabs
   (Entities · Schematic · Flow · Ports · Waveforms · Runs · Files · Output)
   fit without the last three sliding off-screen. Real-device audit
   2026-08-02 on a 1080 × 2400 @ 420dpi Xiaomi 13T (411 CSS px viewport)
   showed only 5.5 tabs visible with the previous 380 px threshold: 8 × 64
   min-width + chevron reserve exceeded the available width and the
   horizontal-scroll affordance (mask fade) was too subtle for touch users
   to discover. Long-press on any tab still surfaces its label via
   KmilaTooltip. */
@media (max-width: 480px) {
    .kmila-dock__tab-label { display: none; }
    .kmila-dock__tab { flex-direction: row; min-width: 44px; }
}


/* --- Panel / tab content --- */

.kmila-dock__panel {
    flex: 1 1 auto;
    /* Both min-* values must be 0 so the flex child can shrink below its
       content's intrinsic size. Otherwise the inner WaveformViewer's wide
       SVG grid grows this panel past the viewport and no amount of zoom-out
       inside the viewer can undo it. */
    min-width: 0;
    min-height: 0;
    /* Vertical overflow is legitimate (long outputs); horizontal is handled
       by each tab's own scroll container (e.g. .waveform-viewer__body). */
    overflow-x: hidden;
    overflow-y: auto;
    padding: var(--space-4);
}

/* Schematic tab owns its own scrolling on both axes. The default panel
   rules above (overflow-y: auto, padding) would otherwise let the
   schematic's natural canvas push the entire dock — and the editor
   page above it — past the viewport. Drop padding too so the schematic
   gets the full panel rect to scroll inside. */
.kmila-dock__panel--schematic {
    overflow: hidden;
    padding: 0;
}

/* Waveforms tab owns its own internal scroll (see .waveform-viewer__body).
   The default panel rule above used to add a SECOND vertical scrollbar
   alongside the body's — pair of nested overflow:auto's stacked. Same
   recipe as the schematic: the panel becomes a fixed rect; the viewer
   inside fills it and handles overflow internally. Padding stays at 0
   so the toolbar's bottom border sits flush with the panel edge. */
.kmila-dock__panel--waveforms {
    overflow: hidden;
    padding: 0;
    display: flex;
    flex-direction: column;
}


/* ──────────────────────────────────────────────────────────────────
   Schematic export modal — global so the rules survive the Bootstrap
   modal's DOM teleport (scoped CSS on the modal component would not
   reach the rendered nodes outside its parent). Layout breakdown:
     • Form root                .kmila-export-form  → vertical sections
     • Section header           .kmila-export-section__title
     • Output-options grid      .kmila-export-grid  → 2-up auto-fit
     • Scope radio list         .kmila-export-scope → 1 column always
     • Checkbox group           .kmila-export-checks
     • Footer (cancel/submit)   .kmila-export-footer
   Mobile (≤ 600 px): grid collapses to 1 column, footer stacks.
   ────────────────────────────────────────────────────────────────── */
.kmila-export-form {
    padding: var(--space-3) var(--space-4);
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    max-height: 80vh;
    overflow-y: auto;
}

.kmila-export-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.kmila-export-section__title {
    font-size: var(--fs-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
    margin: 0;
}

.kmila-export-form .form-label {
    font-size: var(--fs-xs);
    font-weight: 600;
    color: var(--text-strong);
    margin-bottom: 4px;
}

/* Output-options grid: pairs of (label, control). Auto-fit with a
   220px min so we end up with 2 columns on a wide modal and a
   single column on phones — without ever truncating a label. */
.kmila-export-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--space-3);
}

.kmila-export-field {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.kmila-export-field--inline {
    margin-top: var(--space-2);
    max-width: 280px;
}

/* Compact selects + inputs that match the rest of the app. */
.kmila-export-form .form-select,
.kmila-export-form .form-control {
    border-radius: var(--radius-md);
    border-color: var(--border-subtle);
    background-color: var(--surface-1);
    color: var(--text-strong);
    font-size: var(--fs-sm);
    line-height: 1.3;
    padding: 4px 28px 4px 10px;
    height: auto;
}

.kmila-export-form .form-select:focus,
.kmila-export-form .form-control:focus {
    border-color: var(--brand-accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--kmila-accent) 18%, transparent);
}

/* Scope picker — single column, full width. Each card is one
   horizontal row: radio dot · title above hint. The hint is
   line-clamped to 2 so even very long descriptions never push the
   card height past ~64px. */
.kmila-export-scope {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-2);
}

.kmila-export-scope__opt {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    cursor: pointer;
    margin: 0;
    background: var(--surface-1);
    transition:
        border-color var(--duration-fast) var(--ease-standard),
        background-color var(--duration-fast) var(--ease-standard),
        box-shadow var(--duration-fast) var(--ease-standard);
}

.kmila-export-scope__opt:hover:not(.is-disabled) {
    border-color: var(--brand-accent);
    background: var(--surface-2);
}

.kmila-export-scope__opt input[type="radio"] {
    appearance: none;
    -webkit-appearance: none;
    width: 16px;
    height: 16px;
    border: 2px solid var(--border-strong, rgba(148, 163, 184, 0.6));
    border-radius: 50%;
    margin: 3px 0 0 0;
    flex-shrink: 0;
    position: relative;
    cursor: inherit;
    background: transparent;
}

.kmila-export-scope__opt input[type="radio"]:checked {
    border-color: var(--brand-accent);
}

.kmila-export-scope__opt input[type="radio"]:checked::after {
    content: "";
    position: absolute;
    inset: 2px;
    border-radius: 50%;
    background: var(--brand-accent);
}

.kmila-export-scope__opt:has(input:checked) {
    border-color: var(--brand-accent);
    background: color-mix(in srgb, var(--kmila-accent) 6%, transparent);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--kmila-accent) 18%, transparent);
}

.kmila-export-scope__opt.is-disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.kmila-export-scope__body {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    flex: 1;
}

.kmila-export-scope__body strong {
    font-size: var(--fs-sm);
    color: var(--text-strong);
    font-weight: 600;
    line-height: 1.25;
}

.kmila-export-scope__body small {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    line-height: 1.35;
    /* Two-line clamp so very long hints never bloat the modal. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* Segmented Color/Mono pill. */
.kmila-export-pill {
    display: inline-flex;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--surface-1);
    width: fit-content;
}

.kmila-export-pill__opt {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 4px 10px;
    border: none;
    background: transparent;
    color: var(--text-muted);
    font-size: var(--fs-xs);
    cursor: pointer;
    transition:
        background-color var(--duration-fast) var(--ease-standard),
        color var(--duration-fast) var(--ease-standard);
}

.kmila-export-pill__opt + .kmila-export-pill__opt {
    border-left: 1px solid var(--border-subtle);
}

.kmila-export-pill__opt:hover {
    background: var(--surface-2);
    color: var(--text-strong);
}

.kmila-export-pill__opt.is-active {
    background: var(--brand-accent);
    color: #fff;
}

.kmila-export-pill__opt:focus-visible {
    outline: 2px solid var(--brand-accent);
    outline-offset: 2px;
}

/* Checkbox cards — same visual idiom as the scope cards but with
   a checkbox instead of a radio. Inline children stack on mobile. */
.kmila-export-checks {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.kmila-export-check {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    cursor: pointer;
    margin: 0;
    background: var(--surface-1);
    transition:
        border-color var(--duration-fast) var(--ease-standard),
        background-color var(--duration-fast) var(--ease-standard);
}

.kmila-export-check:hover {
    border-color: var(--brand-accent);
    background: var(--surface-2);
}

.kmila-export-check input[type="checkbox"] {
    margin: 3px 0 0 0;
    flex-shrink: 0;
}

.kmila-export-check__body {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    flex: 1;
}

.kmila-export-check__body strong {
    font-size: var(--fs-sm);
    color: var(--text-strong);
    font-weight: 600;
    line-height: 1.25;
}

.kmila-export-check__body small {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    line-height: 1.35;
}

.kmila-export-form .form-check-input {
    border-color: var(--border-strong, rgba(148, 163, 184, 0.6));
    cursor: pointer;
}

.kmila-export-form .form-check-input:checked {
    background-color: var(--brand-accent);
    border-color: var(--brand-accent);
}

.kmila-export-form .form-check-input:focus {
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--kmila-accent) 18%, transparent);
    border-color: var(--brand-accent);
}

/* Footer — buttons right-aligned on desktop, full-width stacked on
   phones. */
.kmila-export-footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
    padding-top: var(--space-2);
    border-top: 1px solid var(--border-subtle);
}

.kmila-export-footer .btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

@media (max-width: 600px) {
    .kmila-export-form {
        padding: var(--space-3);
        max-height: 75vh;
    }
    .kmila-export-grid {
        grid-template-columns: 1fr;
        gap: var(--space-2);
    }
    .kmila-export-footer {
        flex-direction: column-reverse;
        align-items: stretch;
    }
    .kmila-export-footer .btn {
        justify-content: center;
    }
    .kmila-export-pill {
        width: 100%;
    }
    .kmila-export-pill__opt {
        flex: 1;
        justify-content: center;
    }
    .kmila-export-scope__body small {
        -webkit-line-clamp: 3;
    }
}


/* --- Empty state (shown when a tab has no content yet) --- */

.kmila-dock__empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    padding: var(--space-7) var(--space-4);
    color: var(--text-muted);
    text-align: center;
}

.kmila-dock__empty-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    background-color: var(--surface-3);
    color: var(--text-subtle);
    border-radius: var(--radius-pill);
    font-size: var(--fs-xl);
}

.kmila-dock__empty p {
    margin: 0;
    max-width: 44ch;
    line-height: var(--lh-relaxed);
}


/* ============================================================================
   Button — Ghost variant (minimal, for list-row actions and icon buttons)
   ============================================================================ */

.btn-ghost {
    background-color: transparent;
    border: 1px solid transparent;
    color: var(--text-muted);
    font-weight: var(--fw-medium);
    padding: var(--space-1) var(--space-2);
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    border-radius: var(--radius-sm);
    transition: background-color var(--duration-fast) var(--ease-standard),
                color var(--duration-fast) var(--ease-standard);
}

.btn-ghost:hover {
    background-color: var(--surface-3);
    color: var(--text-strong);
    border-color: transparent;
}

.btn-ghost:focus-visible {
    outline: 2px solid var(--kmila-accent);
    outline-offset: 2px;
}

.btn-ghost.btn-sm {
    padding: 4px 8px;
    font-size: var(--fs-sm);
    min-height: auto;
}


/* ============================================================================
   Files list (Files dock tab)
   ============================================================================ */

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

.files-list__header {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border-subtle);
    color: var(--text-strong);
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
}

.files-list__header .bi { color: var(--kmila-accent); }

.files-list__title { flex: 1 1 auto; }

.files-list__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 22px;
    height: 20px;
    padding: 0 8px;
    background-color: var(--surface-3);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    border-radius: var(--radius-pill);
}

.files-list__items {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}


/* Empty state */
.files-list__empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-6) var(--space-4);
    text-align: center;
    color: var(--text-muted);
}

.files-list__empty-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    background-color: var(--surface-3);
    color: var(--text-subtle);
    border-radius: var(--radius-pill);
    font-size: var(--fs-lg);
    margin-bottom: var(--space-1);
}

.files-list__empty p { margin: 0; }
.files-list__empty-sub { font-size: var(--fs-sm); }


/* --- File row --- */

.file-row {
    --fr-fg: var(--text-muted);
    --fr-bg: var(--surface-3);

    position: relative;
    display: grid;
    grid-template-columns: auto auto 1fr auto auto auto;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    padding-left: var(--space-3);
    border-radius: var(--radius-sm);
    transition: background-color var(--duration-fast) var(--ease-standard);
}

.file-row:hover {
    background-color: var(--surface-1);
}

.file-row.is-active {
    background-color: var(--kmila-accent-soft);
}


/* Variant tints */
.file-row--vhdl     { --fr-fg: var(--kmila-accent); --fr-bg: var(--kmila-accent-soft); }
.file-row--markdown { --fr-fg: var(--success);      --fr-bg: var(--success-soft); }
.file-row--text     { --fr-fg: var(--text-default); --fr-bg: var(--surface-3); }
.file-row--json     { --fr-fg: var(--warning);      --fr-bg: var(--warning-soft); }
.file-row--xml      { --fr-fg: var(--warning);      --fr-bg: var(--warning-soft); }
.file-row--other    { --fr-fg: var(--text-muted);   --fr-bg: var(--surface-3); }


/* Left-edge indicator (only visible on active row) */
.file-row__indicator {
    position: absolute;
    left: 0;
    top: 6px;
    bottom: 6px;
    width: 3px;
    border-radius: 0 var(--radius-xs) var(--radius-xs) 0;
    background-color: transparent;
    transition: background-color var(--duration-fast) var(--ease-standard);
}

[dir="rtl"] .file-row__indicator {
    left: auto;
    right: 0;
    border-radius: var(--radius-xs) 0 0 var(--radius-xs);
}

.file-row.is-active .file-row__indicator {
    background-color: var(--kmila-accent);
}


/* Icon tile */
.file-row__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    background-color: var(--fr-bg);
    color: var(--fr-fg);
    border-radius: var(--radius-sm);
    flex-shrink: 0;
    /* Anchor for the entry-point marker (absolute child) so the star badge
       hugs the icon's corner regardless of viewport / list zoom. */
    position: relative;
}

/* Entry-point marker — small gold star badge overlapping the file icon's
   top-right corner. Pure visual; semantics live on the parent `.is-entry`
   class and the kebab menu's "Set/Clear entry point" action. */
.file-row__entry-marker {
    position: absolute;
    top: -4px;
    right: -4px;
    width: 14px;
    height: 14px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--warning);
    background-color: var(--surface-1);
    border-radius: 50%;
    font-size: var(--fs-xs);
    /* Subtle ring so the badge reads as detached even on dark/coloured
       icon backgrounds. Surface-1 matches the row hover surface, keeping
       the badge legible in both states. */
    box-shadow: 0 0 0 2px var(--surface-1);
}

.file-row.is-entry .file-row__name {
    /* Entry-point file gets a slight visual emphasis so it's findable at a
       glance in long file lists. Not bold (already medium) — just nudges
       the colour to the strong text token. */
    color: var(--text-strong);
}


/* Filename (button so it's clickable to open) */
.file-row__name {
    grid-column: 3;
    background: none;
    border: none;
    padding: 0;
    text-align: left;
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    color: var(--text-default);
    cursor: pointer;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
    transition: color var(--duration-fast) var(--ease-standard);
}

.file-row__name:hover {
    color: var(--kmila-accent);
}

.file-row.is-active .file-row__name {
    color: var(--kmila-accent);
    font-weight: var(--fw-semibold);
}


/* Rename input (replaces the name when editing) */
.file-row__rename {
    grid-column: 3;
    width: 100%;
    padding: 4px 8px;
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    color: var(--text-strong);
    background-color: var(--surface-2);
    border: 1px solid var(--kmila-accent);
    border-radius: var(--radius-xs);
    box-shadow: var(--shadow-focus);
}

.file-row__rename:focus {
    outline: none;
}


/* Extension chip */
.file-row__chip {
    display: inline-flex;
    align-items: center;
    padding: 1px 6px;
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    color: var(--fr-fg);
    background-color: var(--fr-bg);
    border-radius: var(--radius-xs);
    flex-shrink: 0;
}

/* On phones the icon tile already encodes the extension via its tint
   (.file-row__icon variants), so the parallel `.vhdl` chip is visual
   redundancy that pushes the filename into ellipsis. Hide the chip and
   reclaim the width for the name. */
@media (max-width: 640.98px) {
    .file-row__chip { display: none; }
}


/* Inline "Open" action */
.file-row__open {
    flex-shrink: 0;
}

/* On very narrow screens, the Open button collapses to icon-only */
@media (max-width: 480px) {
    .file-row__open span { display: none; }
}


/* --- Kebab menu --- */

.file-row__menu-btn {
    flex-shrink: 0;
    cursor: pointer;
    padding: 4px 6px;
}

.file-row__menu-btn.is-open {
    background-color: var(--surface-3);
    color: var(--text-strong);
}

/* Backdrop — invisible click-catcher behind the open panel. Closes the menu
   when the user clicks outside it. Sits below --z-popover so the panel above
   keeps interactivity. */
.file-row__menu-backdrop {
    position: fixed;
    inset: 0;
    z-index: calc(var(--z-popover) - 1);
    background: transparent;
}

.file-row__menu-panel {
    /* Position is set inline via Blazor — top/left come from JS interop after
       the user clicks a trigger. Fixed positioning lets the panel escape any
       ancestor with overflow:hidden (dock card, scroll container, etc.).
       max-height + overflow caps very long menus (rare, but a safety net for
       when the C#-side flip-up calc estimates short and the panel actually
       grows past the viewport). See [[project_file_menu_overflow]]. */
    position: fixed;
    z-index: var(--z-popover);
    min-width: 180px;
    max-width: calc(100vw - 16px);
    max-height: 80vh;
    overflow-y: auto;
    padding: 4px;
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    display: flex;
    flex-direction: column;
    gap: 2px;
    animation: kmilaPop var(--duration-fast) var(--ease-standard);
}

.file-row__menu-item {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--text-default);
    font-size: var(--fs-sm);
    text-align: left;
    cursor: pointer;
    /* Long translations (de/fr/ar "Move to: <long folder>" etc.) need to wrap
       inside the capped panel width — `nowrap` clipped them off-screen. */
    white-space: normal;
    overflow-wrap: anywhere;
    line-height: var(--lh-snug, 1.3);
    transition: background-color var(--duration-fast) var(--ease-standard);
}

.file-row__menu-item > span {
    flex: 1 1 auto;
    min-width: 0;
}

.file-row__menu-item > .bi {
    flex-shrink: 0;
}

[dir="rtl"] .file-row__menu-item { text-align: right; }

.file-row__menu-item:hover {
    background-color: var(--surface-3);
    color: var(--text-strong);
}

.file-row__menu-item--danger {
    color: var(--danger);
}

.file-row__menu-item--danger:hover {
    background-color: var(--danger-soft);
    color: var(--danger);
}

/* ---- Tree extras (folders, drag-and-drop, toolbar) -------------------------- */

.files-list__toolbar {
    display: inline-flex;
    gap: var(--space-1);
    margin-left: auto;
}

.files-list__toolbar .btn {
    padding: var(--space-1) var(--space-2);
}

[dir="rtl"] .files-list__toolbar { margin-left: 0; margin-right: auto; }

.files-tree { padding-inline-start: 0; }

.file-row--folder {
    --fr-fg: var(--text-strong);
    --fr-bg: var(--surface-3);
    font-weight: 500;
}

.file-row--folder.is-drop {
    outline: 2px dashed var(--kmila-accent);
    outline-offset: -2px;
    background: var(--kmila-accent-soft);
}

.file-row--folder.is-editing {
    background: var(--surface-2);
}

.file-row__chevron {
    flex: 0 0 auto;
    width: 1.25rem;
    height: 1.25rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    cursor: pointer;
    padding: 0;
    transition: background-color var(--duration-fast) var(--ease-standard);
}

.file-row__chevron:hover {
    background-color: var(--surface-3);
    color: var(--text-strong);
}

[draggable="true"]:active {
    cursor: grabbing;
}

/* ---- Flow diagram dock tab -------------------------------------------------
   Styled to match the schematic dock surface; mermaid handles the inner SVG
   colors via its own theme + our classDef overrides. The body scrolls so a
   tall flowchart doesn't push the dock off-screen. */

.flow-diagram {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
}

.flow-diagram__head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
    background: var(--surface-2);
    font-weight: 500;
}

.flow-diagram__head .bi {
    color: var(--kmila-accent);
}

.flow-diagram__count {
    background: var(--surface-3);
    border-radius: 999px;
    padding: 2px 8px;
    font-size: var(--fs-sm);
    color: var(--text-muted);
}

.flow-diagram__head .btn {
    margin-left: auto;
}

.flow-diagram__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    padding: var(--space-3);
}

.flow-diagram__body pre.mermaid {
    margin: 0;
    background: transparent;
    border: 0;
}

.flow-diagram__empty {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    color: var(--text-muted);
    padding: var(--space-5);
    text-align: center;
}

.flow-diagram__empty .bi {
    font-size: var(--fs-xl);
    opacity: 0.6;
}


/* ============================================================================
   Entity panel (Entities dock tab)
   ============================================================================ */

.entity-panel {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: 0;
}

.entity-panel + .entity-panel {
    margin-top: var(--space-5);
    padding-top: var(--space-5);
    border-top: 1px solid var(--border-subtle);
}


/* Header — VHDL-flavored title */

.entity-panel__header {
    display: flex;
    /* Was flex-wrap:wrap — when the entity name + IN/OUT chips didn't fit on
       one line, the chips dropped to a new row which read as broken layout.
       Keep one row; long names ellipsize via __name's text-overflow rule. */
    flex-wrap: nowrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    min-width: 0;
}

.entity-panel__title {
    margin: 0;
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--fs-lg);
    font-weight: var(--fw-regular);
    line-height: 1.2;
    color: var(--text-default);
    min-width: 0;
}

.entity-panel__kw {
    color: var(--kmila-accent);
    font-weight: var(--fw-semibold);
}

.entity-panel__name {
    color: var(--text-strong);
    font-weight: var(--fw-bold);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}


/* Port-count chips next to the title */

.entity-panel__meta {
    display: inline-flex;
    gap: var(--space-2);
    flex-shrink: 0;
}

.entity-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 10px;
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    border-radius: var(--radius-pill);
    border: 1px solid transparent;
}

.entity-chip--in  {
    color: var(--success);
    background-color: var(--success-soft);
}

.entity-chip--out {
    color: var(--kmila-accent);
    background-color: var(--kmila-accent-soft);
}


/* Empty-ports state */

.entity-panel__empty {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    padding: var(--space-3) var(--space-4);
    background-color: var(--surface-1);
    border: 1px dashed var(--border-default);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-size: var(--fs-sm);
}


/* ============================================================================
   Signal list — port rows inside an entity
   ============================================================================ */

.signal-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
    background-color: var(--border-subtle);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    overflow: hidden;
}

.signal-row {
    --sr-fg: var(--success);

    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-2);
}

/* When at least one port has a simulated value, expand the grid to make room */
.signal-list--with-values .signal-row {
    grid-template-columns: auto 1fr auto auto;
}

.signal-row--in  { --sr-fg: var(--success); }
.signal-row--out { --sr-fg: var(--kmila-accent); }


/* Direction icon */

.signal-row__dir {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: var(--fs-md);
    color: var(--sr-fg);
    flex-shrink: 0;
}


/* Port name */

.signal-row__name {
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}


/* Data type chip */

.signal-row__type {
    display: inline-flex;
    align-items: center;
    padding: 1px 8px;
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    color: var(--text-muted);
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-xs);
    white-space: nowrap;
    flex-shrink: 0;
}


/* Current value (only rendered when sim has populated values) */

.signal-row__value {
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-weight: var(--fw-bold);
    color: var(--sr-fg);
    padding: 2px 8px;
    background-color: color-mix(in srgb, var(--sr-fg) 12%, transparent);
    border-radius: var(--radius-xs);
    text-align: right;
    min-width: 3ch;
    /* Vector values like "0010_1111" should stay on one line; wrap inside a
       monospace value chip looks like a layout bug. */
    white-space: nowrap;
}

.signal-row__value.is-unset {
    color: var(--text-subtle);
    background-color: transparent;
    font-weight: var(--fw-regular);
}


/* Responsive: stack type + value below name on narrow widths */

@media (max-width: 520px) {
    .signal-row,
    .signal-list--with-values .signal-row {
        grid-template-columns: auto 1fr;
        grid-template-areas:
            "dir name"
            "dir meta";
        row-gap: 2px;
    }
    .signal-row__dir  { grid-area: dir; align-self: start; padding-top: 3px; }
    .signal-row__name { grid-area: name; }
    .signal-row__type,
    .signal-row__value {
        grid-area: meta;
        justify-self: start;
    }
    .signal-list--with-values .signal-row {
        grid-template-areas:
            "dir name"
            "dir meta"
            "dir value";
    }
    .signal-list--with-values .signal-row__value {
        grid-area: value;
        justify-self: start;
        text-align: left;
    }
}


/* File-row responsive — stack the Open button + kebab below on very narrow */

@media (max-width: 480px) {
    .file-row {
        grid-template-columns: auto auto 1fr auto auto;
    }
    .file-row__chip { grid-row: 2; grid-column: 3 / -1; justify-self: start; }
}


/* ============================================================================
   Editor workbench now stacks single-column (ButtonCollapse rail removed)
   ============================================================================ */

.editor-workbench__body {
    display: block;
}

.editor-workbench__body > .editor-monaco,
.editor-workbench__body > .editor-split {
    width: 100%;
}

/* ─── Multi-pane workbench (Phase 2) ──────────────────────────────────
   The body owns the height for both single- and split-pane modes, so
   each pane is bounded by the parent rather than imposing its own
   60dvh from the global .editor-monaco rule. Without this constraint a
   split adds a second 60dvh editor next to the first and the workbench
   grows below the viewport, pushing the run bar / dock out of sight.
*/
.editor-workbench__body--panes {
    display: flex;
    flex-direction: row;
    align-items: stretch;
    gap: 0;
    min-width: 0;
    /* Same height contract that the global .editor-monaco rule used to
       impose by itself. Now applied to the container so panes share it
       and Monaco below stretches to fit. */
    height: 60dvh;
    min-height: 400px;
}

@media (max-width: 768px) {
    /* F-023 (Round 10 2026-08-08): the fixed `55dvh` above extended the
       workbench body 234 CSS px behind the fixed RunBar + Dock overlays,
       leaving a dark empty band inside the Monaco viewport for any file
       shorter than the visible line count and wasting render work. On phones
       the shell already reserves the bottom chrome via `padding-bottom:
       calc(--dock-h + --runbar-h + --statusbar-h + safe-area)` at
       `.kmila-shell__main:has(.editor-page)`, so the parent flex chain
       (`.editor-page → .editor-workbench → __body--panes`) can flex-fill
       exactly the visible slot when we drop the fixed height. `min-height`
       stays so the pane never collapses below the tap-target floor. */
    .editor-workbench__body--panes {
        height: auto;
        flex: 1 1 auto;
        min-height: 220px;
    }
}

.editor-pane {
    display: flex;
    flex-direction: column;
    flex: 1 1 0;
    min-width: 0;
    min-height: 0;       /* lets Monaco shrink within the body height */
    height: 100%;        /* fill the body — important for the unsplit
                            case so a single pane occupies all the
                            space pane B left behind */
    /* When focused (clicked into), draw a 1-pixel accent line on the
       left edge so the user can tell where new files will land. Subtle
       enough to not compete with the actual editor content. */
    border-left: 2px solid transparent;
    transition: border-color var(--duration-fast) var(--ease-standard);
}

/* Tab strip is fixed-height; Monaco fills the rest. */
.editor-pane > .editor-tabs { flex: 0 0 auto; }

/* Override the global `.editor-monaco { height: 60dvh; min-height: 400px }`
   rule when the editor is inside a pane — the pane already owns the
   height contract via the body. */
.editor-pane > .editor-monaco,
.editor-pane > .editor-split {
    flex: 1 1 auto;
    min-height: 0;
    height: auto;
}

/* Only highlight focus when there's more than one pane — a single
   pane is always focused, so the accent would just be visual noise. */
.editor-workbench__body--panes.is-split .editor-pane.is-focused {
    border-left-color: var(--kmila-accent);
}

/* Vertical separator between the two panes — just a 1px border so the
   layout reads as "two columns" instead of "one weird wide editor". */
.editor-workbench__body--panes.is-split > .editor-pane + .editor-pane {
    border-left-color: var(--border-subtle);
}

.editor-workbench__body--panes.is-split > .editor-pane + .editor-pane.is-focused {
    border-left-color: var(--kmila-accent);
}

/* Inside each pane, Monaco / the markdown split fills the remaining
   height below the tab strip. */
.editor-pane > .editor-monaco,
.editor-pane > .editor-split {
    flex: 1 1 auto;
    min-height: 0;
}

/* ─── Split-toggle button (lives in the workbench header) ───────── */
.editor-split-toggle {
    /* Compact ghost button matching the rest of the header chrome.
       Hidden below 900 px viewport widths because the split layout
       isn't usable on phone-portrait — the user falls back to a
       single pane and accumulated tabs render in one strip. */
    margin-right: var(--space-2);
}

@media (max-width: 900px) {
    /* buttons.css loads after pages.css and sets `.kmila-btn { display:
       inline-flex }` at the same specificity, which beats a bare class
       rule on cascade. Use the compound `.kmila-btn.editor-split-toggle`
       so this rule wins without `!important`. The split layout shrinks
       each pane below the readable width for VHDL keywords on phones
       (~200 CSS px per pane wraps "STD_LOGIC" mid-word). */
    .kmila-btn.editor-split-toggle { display: none; }
}


/* ============================================================================
   RunBar — horizontal action strip between Monaco and the dock
   ============================================================================ */

.run-bar {
    position: relative;
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-1);
    /* The bar reacts to its OWN width, not the viewport — a split / narrow
       editor dock has a cramped bar even on a wide screen, which viewport
       media queries can't see. The responsive ladder lives in
       `@container runbar (...)` blocks below. */
    container-type: inline-size;
    container-name: runbar;
    /* overflow:hidden was removed: the burger dropdown is now a plain
       absolute panel anchored to the bar and must escape the card edges.
       The progress line rounds its own bottom corners instead (see below).
       Note: container-type implies `contain: layout`, but NOT paint
       containment, so absolute children still render outside the box. */
}

.run-bar__row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    /* One row, always. The ladder sheds content before it would ever wrap,
       so flex-wrap is off — wrapping was the whole UX complaint. */
    flex-wrap: nowrap;
    justify-content: space-between;
}

/* Config cluster (FPGA · duration · clock). Grows to absorb the slack so the
   action cluster is pushed to the trailing edge; min-width:0 lets the FPGA
   chip ellipsis when the bar gets tight instead of forcing a wrap. */
.run-bar__config {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    flex: 1 1 auto;
}

/* Phone-only consolidated trigger — hidden on desktop where the three
   pills (FPGA · duration · clock) fit comfortably inline. Below the TIGHT
   container breakpoint (≤ 640 px) the inline cluster is hidden and this
   chip becomes the only entry-point to those controls. */
.run-bar__config-trigger {
    display: none;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    background-color: var(--surface-3);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-pill);
    color: var(--text-default);
    font-size: var(--fs-sm);
    height: 36px;
    min-width: 120px;
    max-width: 100%;
    flex: 1 1 0;
    cursor: pointer;
}
.run-bar__config-trigger:hover {
    background-color: var(--surface-4);
}
.run-bar__config-trigger .bi {
    color: var(--kmila-accent);
    flex-shrink: 0;
}
.run-bar__config-trigger--missing .bi:first-child {
    color: var(--danger);
}
.run-bar__config-trigger-model {
    font-family: var(--font-mono);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
.run-bar__config-trigger-sep { color: var(--text-muted); flex-shrink: 0; }
.run-bar__config-trigger-clock {
    color: var(--text-default);
    font-family: var(--font-mono);
    font-weight: var(--fw-semibold);
    flex-shrink: 0;
}
.run-bar__config-trigger-chevron {
    color: var(--text-muted);
    flex-shrink: 0;
    transition: transform var(--duration-fast) var(--ease-standard);
}
.run-bar__config-trigger.is-open .run-bar__config-trigger-chevron {
    transform: rotate(180deg);
}

/* Backdrop sits between the open popover and the rest of the editor on
   phones — a tap anywhere outside the panel closes it. CSS hides it on
   desktop where the inline cluster never enters popover mode. */
.run-bar__config-backdrop {
    display: none;
}

/* Action cluster (Build · Run · Stop · Save · New · burger). Never shrinks —
   the buttons stay their natural size; the ladder hides them in stages.
   Action gap widened from space-2 → space-3 (8 → 12 px) on desktop so the
   four pill buttons (Build / Run / Save / New) don't read as one strip of
   chrome (audit 2026-06-14, user feedback "compile/execute/run too close"). */
.run-bar__actions {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex: 0 0 auto;
}

/* Visual hairline between the config cluster (FPGA · time · clock) and the
   action cluster (Build · Run · Save · New · burger) — without it the eight
   pill chrome rectangles bleed together on a wide bar. The pseudo-element
   sits inside the action cluster so it disappears together with the
   buttons it precedes when the responsive ladder folds them away. */
.run-bar__actions::before {
    content: "";
    align-self: stretch;
    width: 1px;
    margin-inline-end: var(--space-1);
    background: var(--border-subtle);
}
@container runbar (max-width: 640px) {
    .run-bar__actions::before { display: none; }
}


/* FPGA reference chip */
.run-bar__fpga {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    background-color: var(--surface-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    /* Fluid width: never less than 220 (model names need room), can grow
       to 360 on desktop so the "Selecciona un FPGA primero" warning fits
       without truncation. Narrow container query below tightens it back
       on phones. */
    max-width: 360px;
    min-width: 0;
    color: var(--text-default);
    font-size: var(--fs-sm);
}

/* When no FPGA is selected the chip carries the user's only cue about
   why Build/Run are disabled — keep it visible enough on wider runbars
   without crowding out the time/scale/clock pills that share the row.
   220 px fits "Select an FPGA first" / "Selecciona un FPGA prim…" with
   a graceful ellipsis instead of the harsh "FPG…" the old 200 px cap
   produced. Narrow-container query below still tightens on phones. */
@container runbar (min-width: 700px) {
    .run-bar__fpga--missing { min-width: 220px; }
}

.run-bar__fpga .bi { color: var(--kmila-accent); flex-shrink: 0; }

.run-bar__fpga-model {
    font-family: var(--font-mono);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.run-bar__fpga-empty {
    color: var(--text-muted);
    font-style: italic;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}


/* Time input + scale select — share a single pill background */
.run-bar__time {
    display: inline-flex;
    align-items: center;
    background-color: var(--surface-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    overflow: hidden;
    height: 36px;
}

.run-bar__time:focus-within {
    border-color: var(--kmila-accent);
    box-shadow: var(--shadow-focus);
}

.run-bar__time-icon {
    padding: 0 8px 0 12px;
    color: var(--text-muted);
    font-size: var(--fs-base);
}

.run-bar__time-input {
    width: 80px;
    border: none;
    background: transparent;
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    padding: 0 8px;
    outline: none;
}

/* Remove native number spinner chrome — looks sloppy inside the pill */
.run-bar__time-input::-webkit-outer-spin-button,
.run-bar__time-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.run-bar__time-input[type=number] { -moz-appearance: textfield; }

.run-bar__scale {
    border: none;
    border-left: 1px solid var(--border-subtle);
    background-color: transparent;
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    color: var(--text-default);
    padding: 0 var(--space-3);
    height: 100%;
    cursor: pointer;
    outline: none;
}

[dir="rtl"] .run-bar__scale {
    border-left: none;
    border-right: 1px solid var(--border-subtle);
}


/* Build button (outline-secondary) */
.run-bar__build {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    white-space: nowrap;
    flex-shrink: 0;
}

/* Run button (primary) */
.run-bar__run {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    white-space: nowrap;
    flex-shrink: 0;
    min-width: 120px;
    justify-content: center;
}

/* Stop button — compact square with a red hue */
.run-bar__stop {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    padding: 0;
    color: var(--danger);
    flex-shrink: 0;
}

.run-bar__stop:hover {
    background-color: var(--danger-soft);
    color: var(--danger);
}


/* Overflow burger menu — hidden on desktop (the inline Save / New File /
   Build buttons cover this), shown ≤768 px so the bar fits one row on
   phones. A Blazor-driven `.is-open` toggle replaces the older details/
   summary pattern so we can close the menu from C# after each item action. */
.run-bar__overflow {
    position: relative;
    flex-shrink: 0;
    display: none;          /* mobile-only; overridden in the @media block */
}

.run-bar__overflow.is-open .run-bar__overflow-btn {
    background-color: var(--surface-3);
    color: var(--text-strong);
    /* Raise the burger above the backdrop while the menu is open so a
       second tap closes the menu by hitting the burger again — not by
       landing on the fixed-position backdrop that otherwise covers it. */
    position: relative;
    z-index: calc(var(--z-dropdown) + 1);
}

/* Transparent click-catcher behind the menu so tapping anywhere outside
   closes it. Sits below the panel but above everything else on screen. */
.run-bar__overflow-backdrop {
    position: fixed;
    inset: 0;
    z-index: calc(var(--z-dropdown) - 1);
    background: transparent;
}

.run-bar__overflow-panel {
    position: absolute;
    top: calc(100% + 4px);
    right: 0;
    left: auto;
    z-index: var(--z-dropdown);
    min-width: 220px;
    padding: 4px;
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    display: flex;
    flex-direction: column;
    gap: 2px;
    animation: kmilaPop var(--duration-fast) var(--ease-standard);
}

[dir="rtl"] .run-bar__overflow-panel {
    right: auto;
    left: 0;
}

.run-bar__overflow-item {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--text-default);
    font-size: var(--fs-sm);
    text-align: left;
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-standard);
}

.run-bar__overflow-item:hover {
    background-color: var(--surface-3);
    color: var(--text-strong);
}

.run-bar__overflow-item > span:first-of-type { flex: 1 1 auto; }

.run-bar__shortcut {
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    color: var(--text-muted);
    background-color: var(--surface-3);
    border: 1px solid var(--border-subtle);
    padding: 1px 6px;
    border-radius: var(--radius-xs);
}


/* Progress bar under the row — fills L→R during running. Rounds its own
   bottom corners + clips the fill, since `.run-bar` no longer has
   overflow:hidden (the burger dropdown needs to escape the card). */
.run-bar__progress {
    height: 3px;
    background-color: var(--surface-3);
    border-bottom-left-radius: var(--radius-lg);
    border-bottom-right-radius: var(--radius-lg);
    overflow: hidden;
}

.run-bar__progress-fill {
    display: block;
    height: 100%;
    background-color: var(--kmila-accent);
    transition: width var(--duration-base) var(--ease-standard);
}

/* Percent + live time-remaining estimate under the progress line. */
.run-bar__progress-meta {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-2);
    padding: 2px var(--space-3);
    font-size: var(--fs-2xs);
    color: var(--text-muted);
}
.run-bar__progress-pct {
    font-family: var(--font-mono);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
}
.run-bar__progress-eta { font-variant-numeric: tabular-nums; }

.run-bar--running .run-bar__progress-fill {
    background: linear-gradient(
        90deg,
        var(--kmila-accent) 0%,
        color-mix(in srgb, var(--kmila-accent) 60%, transparent) 50%,
        var(--kmila-accent) 100%
    );
    background-size: 200% 100%;
    animation: kmilaShimmerProgress 1.8s linear infinite;
}

@keyframes kmilaShimmerProgress {
    from { background-position: 200% 0; }
    to   { background-position: -200% 0; }
}


/* ── Responsive ladder — keyed on the BAR's own width (container query) ──
   The bar degrades in deliberate stages instead of wrapping. Each stage is
   a max-width on the `runbar` container, so a split editor dock collapses
   exactly like a small screen does — same model, both cases.

   ROOMY  (default, > 860px) : everything with labels.
   SNUG   (≤ 860px)          : secondary action labels → icon-only.
   TIGHT  (≤ 640px)          : Build + Save + New fold into the burger.
   CRAMPED(≤ 430px)          : compress config pills; Run grows to fill. */

/* SNUG — Build / Save / New shed their text, keep their icons. Run (the
   primary CTA) keeps its label. */
@container runbar (max-width: 860px) {
    .run-bar__btn-label,
    .run-bar__file-btn-label { display: none; }
    .run-bar__build { min-width: 0; }
}

/* TIGHT — fold the secondary actions into the burger. Run + Stop stay
   inline so you can always react to a running sim in one tap. The compound
   `.run-bar` selector wins over `.kmila-btn { display:inline-flex }` from
   buttons.css (loaded after pages.css). */
@container runbar (max-width: 640px) {
    .run-bar .run-bar__build,
    .run-bar .run-bar__file-btn { display: none; }
    .run-bar__overflow { display: inline-flex; }
}

/* Collapse FPGA + duration + clock into a single popover behind a chip on
   phones. The inline `.run-bar__config` cluster used to sit in the row
   beside Run/Stop and visually overlapped them on narrow phones (audit
   2026-06-16). We use a viewport media query here (not the runbar
   container query) so the trigger is guaranteed to show on any
   phone-sized viewport even if the runbar's container width measurement
   bottoms out before the descendant rule applies. */
@media (max-width: 768px) {
    .run-bar__config-trigger { display: inline-flex; }
    .run-bar .run-bar__config {
        position: absolute;
        bottom: calc(100% + 8px);
        left: var(--space-3);
        right: var(--space-3);
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-2);
        padding: var(--space-3);
        background-color: var(--surface-2);
        border: 1px solid var(--border-default);
        border-radius: var(--radius-lg);
        box-shadow: var(--shadow-3);
        z-index: 1061;
        /* Hidden until the user taps the trigger. */
        display: none;
    }
    .run-bar .run-bar__config.is-open { display: flex; }

    .run-bar .run-bar__config > .run-bar__fpga,
    .run-bar .run-bar__config > .run-bar__time,
    .run-bar .run-bar__config > .run-bar__clock {
        width: 100%;
        max-width: 100%;
        justify-content: flex-start;
    }
    .run-bar .run-bar__config .run-bar__time-input {
        flex: 1 1 auto;
        width: auto;
        max-width: none;
    }

    .run-bar__config-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        background: transparent;
        z-index: 1060;
    }
}

/* CRAMPED — tiny phones / very narrow split. Tighten the config pills and
   let Run expand into the leftover width so it stays a comfortable tap
   target. */
@container runbar (max-width: 430px) {
    .run-bar__row { gap: var(--space-2); }
    .run-bar__config { gap: var(--space-1); }
    /* FPGA chip: relax the hard cap; let flex shrink + min-width:0 + inner
       ellipsis size it naturally so long board names like EP4CE22F17C6N
       stay legible without overflowing. */
    .run-bar__fpga {
        max-width: 160px;
        min-width: 0;
        flex: 0 1 auto;
        padding-inline: var(--space-2);
    }
    .run-bar__fpga--missing {
        min-width: 150px;
        max-width: 200px;
        overflow: hidden;
    }
    .run-bar__time-input { width: 56px; }
    .run-bar__clock .run-bar__time-input { max-width: 52px; }
    /* Run is the primary CTA: keep it generous but allow ellipsis on long
       labels ("Reejecutar" in ES) so it never spills outside. */
    .run-bar__run {
        min-width: 96px;
        flex: 1 1 auto;
        overflow: hidden;
    }
    .run-bar__run .run-bar__btn-label {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    /* When FPGA is missing the warning chip is wide and Run is disabled —
       both duration and clock chips are moot until the user picks a board.
       Hide them so the warning chip + disabled Run + burger fit cleanly.
       Once an FPGA is selected the chip shrinks to its model name (~80 px)
       and the duration + clock pills come back automatically. */
    .run-bar__fpga--missing ~ .run-bar__time,
    .run-bar__fpga--missing ~ .run-bar__clock { display: none; }
    /* Compress the duration + clock pills so FPGA + Run + Stop + burger
       coexist on a ~358px usable phone width without overlap. Pad-inline
       drops to space-1, the inline label "Clock" is hidden (the CPU icon
       is enough), and the input narrows. */
    .run-bar__time,
    .run-bar__clock {
        padding-inline: var(--space-1);
        gap: var(--space-1);
    }
    .run-bar__clock-unit { font-size: var(--fs-2xs); padding-inline: 2px; }
    .run-bar__time-icon { width: 14px; height: 14px; }
}

/* Mobile-only: the bar is pinned to the viewport bottom (see the
   max-width:768px block far below), so the burger dropdown must open
   UPWARD instead of down. It's a plain absolute panel anchored to the
   burger now (no more position:fixed) — the old fixed anchor existed only
   to survive flex-wrap, which no longer happens. */
@media (max-width: 768px) {
    .run-bar__overflow-panel {
        top: auto;
        bottom: calc(100% + 4px);
    }
}


/* ============================================================================
   PortsPanel (Ports dock tab body)
   ============================================================================ */

.ports-panel {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
}

.ports-panel__entity + .ports-panel__entity {
    padding-top: var(--space-4);
    border-top: 1px solid var(--border-subtle);
}

.ports-panel__entity-head {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    color: var(--kmila-accent);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
}

.ports-panel__entity-name {
    color: var(--text-strong);
}

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


/* ============================================================================
   OutputPanel (Output dock tab body)
   ============================================================================ */

.output-panel {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    height: 100%;
    min-height: 0;
}

.output-panel__toolbar {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border-subtle);
}

.output-panel__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* em-based sizing so the badge scales with the surrounding text and the
       user's text-size setting (xs..xl) doesn't make it feel cramped/oversized. */
    min-width: 1.6em;
    height: 1.4em;
    padding: 0 0.5em;
    background-color: var(--surface-3);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    border-radius: var(--radius-pill);
    margin-right: auto;
}

[dir="rtl"] .output-panel__count { margin-right: 0; margin-left: auto; }

.output-panel__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
    background-color: var(--border-subtle);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    /* The list grows as more entries arrive; we let it consume the remaining
       dock space (flex: 1) and scroll its own contents instead of overflowing
       the dock body. min-height:0 is mandatory inside a flex column for the
       child to actually shrink — without it overflow:auto never engages. */
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}


/* Output row — three columns: timestamp · icon · message */
.output-row {
    --or-fg: var(--text-muted);
    --or-bg: var(--surface-1);

    display: grid;
    grid-template-columns: auto auto 1fr;
    gap: var(--space-3);
    align-items: baseline;
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-2);
    font-size: var(--fs-sm);
}

.output-row--info     { --or-fg: var(--kmila-accent); --or-bg: var(--kmila-accent-soft); }
.output-row--success  { --or-fg: var(--success);      --or-bg: var(--success-soft); }
.output-row--warning  { --or-fg: var(--warning);      --or-bg: var(--warning-soft); }
.output-row--danger   { --or-fg: var(--danger);       --or-bg: var(--danger-soft); }

.output-row__time {
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    color: var(--text-muted);
    min-width: 8ch;
    text-align: right;
}

.output-row__icon {
    color: var(--or-fg);
    display: inline-flex;
    align-items: center;
    font-size: var(--fs-base);
}

.output-row__msg {
    color: var(--text-default);
    word-break: break-word;
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    line-height: var(--lh-relaxed);
}


/* ============================================================================
   StatusBar — VS Code-style thin bar at the bottom of the Editor page
   ============================================================================ */

.kmila-status-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: 4px var(--space-3);
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    color: var(--text-muted);
    font-size: var(--fs-xs);
    min-height: 32px;
}

.kmila-status-bar__group {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}

.kmila-status-bar__group--start { flex-shrink: 1; min-width: 0; }
.kmila-status-bar__group--end   { flex-shrink: 0; }


/* Unsaved-changes dot — mirrors the editor header but smaller */
.kmila-status-bar__dot {
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background-color: var(--warning);
    box-shadow: 0 0 0 3px var(--warning-soft);
    flex-shrink: 0;
}


/* File name & mono bits — ellipsize so long paths don't blow up the bar */
.kmila-status-bar__file {
    font-family: var(--font-mono);
    color: var(--text-default);
    font-weight: var(--fw-medium);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 30ch;
    min-width: 0;
}

.kmila-status-bar__pos {
    font-family: var(--font-mono);
    white-space: nowrap;
}

.kmila-status-bar__lang {
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--kmila-accent);
    padding: 1px 6px;
    background-color: var(--kmila-accent-soft);
    border-radius: var(--radius-xs);
}

.kmila-status-bar__sep {
    color: var(--text-subtle);
    user-select: none;
}


/* --- Individual stat chip (Build / Resource / Sim) --- */

.kmila-status-bar__stat {
    --st-fg: var(--text-muted);

    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    background: transparent;
    border: none;
    border-radius: var(--radius-xs);
    color: var(--st-fg);
    font-size: var(--fs-xs);
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-standard),
                color var(--duration-fast) var(--ease-standard);
}

.kmila-status-bar__stat:hover:not(:disabled):not(.kmila-status-bar__stat--readonly) {
    background-color: var(--surface-3);
}

.kmila-status-bar__stat:focus-visible {
    outline: 2px solid var(--kmila-accent);
    outline-offset: -2px;
}

.kmila-status-bar__stat:disabled,
.kmila-status-bar__stat--readonly {
    cursor: default;
}

.kmila-status-bar__stat--neutral { --st-fg: var(--text-muted); }
.kmila-status-bar__stat--info    { --st-fg: var(--kmila-accent); }
.kmila-status-bar__stat--success { --st-fg: var(--success); }
.kmila-status-bar__stat--warning { --st-fg: var(--warning); }
.kmila-status-bar__stat--danger  { --st-fg: var(--danger); }


.kmila-status-bar__label {
    color: var(--text-muted);
    font-weight: var(--fw-medium);
}

.kmila-status-bar__value {
    color: var(--st-fg);
    font-weight: var(--fw-semibold);
    font-family: var(--font-mono);
}

.kmila-status-bar__muted {
    color: var(--text-subtle);
    font-family: var(--font-mono);
}

.kmila-status-bar__mono {
    color: var(--text-default);
    font-family: var(--font-mono);
    font-weight: var(--fw-medium);
}


/* --- Responsive --- */

@media (max-width: 640.98px) {
    /* Drop the fluff on the left and in the middle; keep the critical bits. */
    .kmila-status-bar__pos,
    .kmila-status-bar__lang,
    .kmila-status-bar__sep,
    .kmila-status-bar__muted {
        display: none;
    }
    .kmila-status-bar__file { max-width: 18ch; }
    .kmila-status-bar__label { display: none; }
}


/* ============================================================================
   ConfigurationsPill (TopBar) — save/load named simulation configs
   ============================================================================ */

.kmila-config-pill {
    position: relative;
    flex-shrink: 0;
}

.kmila-config-pill__summary {
    list-style: none;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    /* Was hardcoded rgba white + #ffffff text — designed for a dark topbar
       that no longer exists. On a light topbar the text was rendered white
       on white = invisible. Switch to theme-aware tokens so the pill stays
       legible in both modes (audit 2026-06-14, finding A-25). */
    background-color: var(--surface-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    color: var(--text-strong);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    max-width: 220px;
    user-select: none;
    transition: background-color var(--duration-fast) var(--ease-standard),
                border-color var(--duration-fast) var(--ease-standard);
}

.kmila-config-pill__summary::-webkit-details-marker { display: none; }
.kmila-config-pill__summary::marker { display: none; content: ""; }

.kmila-config-pill__summary:hover {
    background-color: var(--surface-2);
    border-color: var(--border-default);
}

.kmila-config-pill[open] > .kmila-config-pill__summary {
    background-color: var(--surface-2);
    border-color: var(--kmila-accent);
    box-shadow: var(--shadow-focus);
}

.kmila-config-pill__name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
    max-width: 140px;
}

/* TB-2 (editor-mobile audit 2026-06-29): on phones, the pill duplicates
   the project name in the breadcrumb and squeezes the profile cluster.
   Collapse to icon-only (Bookmarks icon + dirty dot + chevron). Tap
   reveals the full panel with all config affordances. */
@media (max-width: 640.98px) {
    .kmila-config-pill__summary { padding: var(--space-1) var(--space-2); }
    .kmila-config-pill__summary .kmila-config-pill__name { display: none; }
}

.kmila-config-pill__dirty {
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background-color: var(--warning);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--warning) 30%, transparent);
    flex-shrink: 0;
}

.kmila-config-pill__chevron {
    color: rgba(255, 255, 255, 0.7);
    font-size: var(--fs-sm);
    transition: transform var(--duration-fast) var(--ease-standard);
}

.kmila-config-pill[open] .kmila-config-pill__chevron {
    transform: rotate(180deg);
}


/* --- Panel --- */

.kmila-config-pill__panel {
    position: absolute;
    top: calc(100% + var(--space-2));
    right: 0;
    left: auto;
    z-index: var(--z-dropdown);
    width: min(320px, 90vw);
    max-height: 480px;
    overflow-y: auto;
    padding: var(--space-2);
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    animation: kmilaPop var(--duration-fast) var(--ease-standard);
}

[dir="rtl"] .kmila-config-pill__panel {
    right: auto;
    left: 0;
}


/* Sections within the popover */

.kmila-config-pill__section {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: var(--space-2) var(--space-1);
}

.kmila-config-pill__section + .kmila-config-pill__section {
    border-top: 1px solid var(--border-subtle);
    padding-top: var(--space-3);
    margin-top: 2px;
}

.kmila-config-pill__label {
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
    padding: 0 var(--space-2);
}


/* Current-config row */

.kmila-config-pill__current {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background-color: var(--kmila-accent-soft);
    color: var(--text-strong);
    border-radius: var(--radius-sm);
    font-size: var(--fs-sm);
}

.kmila-config-pill__current .bi:first-child {
    color: var(--kmila-accent);
}

.kmila-config-pill__current-name {
    flex: 1 1 auto;
    font-weight: var(--fw-semibold);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.kmila-config-pill__modified {
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--warning);
    background-color: var(--warning-soft);
    padding: 1px 6px;
    border-radius: var(--radius-xs);
}


/* Saved-configs list */

.kmila-config-pill__list {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.kmila-config-pill__item {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--text-default);
    font-size: var(--fs-sm);
    text-align: left;
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-standard);
}

.kmila-config-pill__item > span:first-of-type {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.kmila-config-pill__item:hover {
    background-color: var(--surface-3);
}

.kmila-config-pill__item.is-current .bi:first-child {
    color: var(--kmila-accent);
}

.kmila-config-pill__item.is-current {
    color: var(--text-strong);
    font-weight: var(--fw-medium);
}

.kmila-config-pill__item-delete {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    background: none;
    border: none;
    border-radius: var(--radius-xs);
    color: var(--text-muted);
    cursor: pointer;
    opacity: 0;
    transition: opacity var(--duration-fast) var(--ease-standard),
                background-color var(--duration-fast) var(--ease-standard),
                color var(--duration-fast) var(--ease-standard);
}

.kmila-config-pill__item:hover .kmila-config-pill__item-delete {
    opacity: 1;
}

.kmila-config-pill__item-delete:hover {
    background-color: var(--danger-soft);
    color: var(--danger);
}


/* Actions row (Save changes / Save as…) */

.kmila-config-pill__actions {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-1) 0;
    border-top: 1px solid var(--border-subtle);
    margin-top: 2px;
}

.kmila-config-pill__actions > .btn {
    width: 100%;
    justify-content: center;
}


/* Inline save-as input */

.kmila-config-pill__saveas {
    display: flex;
    align-items: center;
    gap: 4px;
}

.kmila-config-pill__saveas .form-control {
    flex: 1 1 auto;
    min-width: 0;
}

.kmila-config-pill__saveas .btn {
    flex-shrink: 0;
    width: auto !important;
    padding: 4px 8px;
}


/* Responsive — on very narrow viewports the pill's name and chevron collapse */
@media (max-width: 575.98px) {
    .kmila-config-pill__name { max-width: 80px; }
    .kmila-config-pill__chevron { display: none; }
}


/* ============================================================================
   PortMomentInput — timeline + moments list
   ============================================================================ */

.port-moment {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-3);
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
}

.port-moment + .port-moment { margin-top: var(--space-3); }

.port-moment--running {
    opacity: 0.72;
    pointer-events: none;
}


/* --- Header row --- */

.port-moment__head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.port-moment__dir {
    display: inline-flex;
    align-items: center;
    color: var(--success);
    font-size: var(--fs-md);
    flex-shrink: 0;
}

.port-moment__name {
    font-family: var(--font-mono);
    font-weight: var(--fw-bold);
    color: var(--text-strong);
    font-size: var(--fs-sm);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.port-moment__type {
    display: inline-flex;
    align-items: center;
    padding: 1px 8px;
    background-color: var(--surface-3);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    border-radius: var(--radius-xs);
}

.port-moment__clock {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    margin-left: auto;
    padding: 4px 10px;
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    color: var(--text-default);
    font-size: var(--fs-xs);
    cursor: pointer;
    user-select: none;
}

[dir="rtl"] .port-moment__clock {
    margin-left: 0;
    margin-right: auto;
}

.port-moment__clock input { margin: 0; flex-shrink: 0; }
.port-moment__clock .bi   { color: var(--kmila-accent); }

.port-moment--clock .port-moment__clock {
    background-color: var(--kmila-accent-soft);
    border-color: var(--kmila-accent);
    color: var(--kmila-accent);
}


/* --- Clock-mode note --- */

.port-moment__clock-note {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    background-color: var(--kmila-accent-soft);
    border: 1px dashed var(--kmila-accent);
    border-radius: var(--radius-sm);
    color: var(--text-default);
    font-size: var(--fs-sm);
}

.port-moment__clock-note .bi {
    color: var(--kmila-accent);
    font-size: var(--fs-md);
    flex-shrink: 0;
    margin-top: 2px;
}

.port-moment__clock-note p { margin: 0; }


/* --- Read-only timeline strip (SVG) --- */

.port-moment__timeline {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.port-moment__timeline-svg {
    width: 100%;
    height: 36px;
    display: block;
    overflow: visible;
}

.port-moment__timeline-axis {
    stroke: var(--border-default);
    stroke-width: 1;
    shape-rendering: crispEdges;
}

.port-moment__timeline-tick {
    stroke: var(--border-default);
    stroke-width: 1;
    shape-rendering: crispEdges;
}

.port-moment__timeline-marker line {
    stroke: var(--kmila-accent);
    stroke-width: 2;
}

.port-moment__timeline-marker circle {
    fill: var(--kmila-accent);
    stroke: var(--surface-2);
    stroke-width: 2;
}

.port-moment__timeline-bounds {
    display: flex;
    justify-content: space-between;
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    color: var(--text-subtle);
    letter-spacing: var(--tracking-wide);
}


/* --- Moments list grid --- */

.port-moment__moments {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.port-moment__grid {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.port-moment__grid-head,
.port-moment__row {
    display: grid;
    grid-template-columns: 90px 80px 1fr 32px;
    gap: var(--space-2);
    align-items: center;
}

.port-moment__grid-head {
    padding: 0 var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
}

.port-moment__row {
    padding: 2px;
    border-radius: var(--radius-sm);
}

.port-moment__row:hover {
    background-color: var(--surface-1);
}

.port-moment__time,
.port-moment__scale,
.port-moment__value {
    font-family: var(--font-mono);
}


/* Value input + optional N/M counter live together */
.port-moment__value-wrap {
    position: relative;
    display: flex;
    align-items: center;
    min-width: 0;
}

.port-moment__value-wrap .port-moment__value {
    width: 100%;
    padding-right: 2.8rem; /* room for the counter pill on the right */
}

.port-moment__value.is-invalid {
    border-color: var(--danger) !important;
    background-color: var(--danger-soft);
    color: var(--danger);
}

.port-moment__counter {
    position: absolute;
    right: 6px;
    top: 50%;
    transform: translateY(-50%);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    color: var(--text-muted);
    background-color: var(--surface-3);
    padding: 1px 6px;
    border-radius: var(--radius-xs);
    pointer-events: none;
    letter-spacing: 0;
}

[dir="rtl"] .port-moment__counter {
    right: auto;
    left: 6px;
}

[dir="rtl"] .port-moment__value-wrap .port-moment__value {
    padding-right: var(--space-3);
    padding-left: 2.8rem;
}

.port-moment__counter.is-invalid {
    color: var(--danger);
    background-color: var(--danger-soft);
}

.port-moment__time::-webkit-outer-spin-button,
.port-moment__time::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.port-moment__time[type=number] { -moz-appearance: textfield; }

.port-moment__remove {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--text-muted);
    padding: 4px;
    border-radius: var(--radius-xs);
}

.port-moment__remove:hover {
    background-color: var(--danger-soft);
    color: var(--danger);
}


/* --- Empty state + add button --- */

.port-moment__empty {
    margin: 0;
    padding: var(--space-3);
    text-align: center;
    color: var(--text-muted);
    font-size: var(--fs-sm);
    font-style: italic;
    border: 1px dashed var(--border-default);
    border-radius: var(--radius-sm);
    background-color: var(--surface-1);
}

.port-moment__add {
    /* UX-N-018: this is the primary CTA on the Ports panel — without it
       students can't define stimuli. Was left-aligned + 120 px wide,
       buried below an empty-state message. Make it a full-width primary
       button so it reads as the obvious next step. */
    align-self: stretch;
    margin-top: var(--space-2);
    padding-block: var(--space-2);
    color: var(--text-on-accent);
    background-color: var(--kmila-accent);
    border-color: var(--kmila-accent);
    font-weight: var(--fw-semibold);
    justify-content: center;
}

.port-moment__add:hover {
    background-color: var(--kmila-accent-strong, var(--kmila-accent));
    color: var(--text-on-accent);
    border-color: var(--kmila-accent-strong, var(--kmila-accent));
    opacity: 0.92;
}


/* --- Responsive — stack the grid into labeled rows on very narrow viewports --- */

@media (max-width: 520px) {
    .port-moment__grid-head {
        display: none;
    }
    .port-moment__row {
        grid-template-columns: 1fr 80px 32px;
        grid-template-areas:
            "value scale remove"
            "time  time  time";
        row-gap: 4px;
    }
    .port-moment__value  { grid-area: value; }
    .port-moment__scale  { grid-area: scale; }
    .port-moment__remove { grid-area: remove; }
    .port-moment__time   { grid-area: time; }
}


/* ============================================================================
   Home (/)
   ============================================================================ */

.home-hero {
    position: relative;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-xl);
    background:
        radial-gradient(ellipse at top left, var(--kmila-accent-soft) 0%, transparent 55%),
        linear-gradient(180deg, var(--surface-1) 0%, var(--surface-2) 100%);
    padding: var(--space-6) var(--space-5);
    margin-bottom: var(--space-7);
    overflow: hidden;
    box-shadow: var(--shadow-1);
}

.home-hero__grid {
    display: grid;
    grid-template-columns: 1.4fr 1fr;
    align-items: center;
    gap: var(--space-6);
}

.home-hero__copy {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    min-width: 0;
}

.home-hero__eyebrow,
.home-section__eyebrow {
    display: inline-block;
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--kmila-accent);
    font-family: var(--font-mono);
}

.home-hero__title {
    /* Fluid scaling between mobile and desktop. The width cap is 36ch (was
       28ch) — under 28ch the longest-locale title (es ≈ 88 chars) wrapped to
       4 lines with "VHDL" alone on the last line. 36ch fits es+de in 3
       balanced lines while still reading as a heading. text-wrap: balance
       redistributes characters so each line is roughly the same length
       (browsers without support fall back to greedy wrap, no regression). */
    font-size: clamp(var(--fs-xl), 1.4rem + 1.6vw, var(--fs-2xl));
    font-weight: var(--fw-bold);
    line-height: var(--lh-tight);
    letter-spacing: var(--tracking-tight);
    color: var(--text-strong);
    margin: 0;
    max-width: 36ch;
    text-wrap: balance;
}

.home-hero__subtitle {
    font-size: var(--fs-md);
    line-height: var(--lh-relaxed);
    color: var(--text-muted);
    margin: 0;
    max-width: 52ch;
    text-wrap: balance;
}

.home-hero__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    align-items: center;
    margin-top: var(--space-3);
}

/* .home-hero__primary, .home-hero__secondary — visual rules moved to
   .kmila-btn (buttons.css). The class names stay in markup as semantic
   anchors; no per-class styles needed any more. */

.home-hero__art {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 220px;
}

.home-hero__logo {
    position: relative;
    z-index: 1;
    max-width: 220px;
    width: 100%;
    height: auto;
    filter: drop-shadow(0 20px 40px rgba(15, 23, 42, 0.18));
}

[data-bs-theme="dark"] .home-hero__logo {
    filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.5));
}


/* Section shell */
.home-overview,
.home-features {
    margin-bottom: var(--space-7);
}

.home-overview__header,
.home-features__header {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-bottom: var(--space-5);
    max-width: 720px;
}

.home-overview__header h2,
.home-features__header h2 {
    margin: 0;
}

.home-overview__body {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-5);
    font-size: var(--fs-base);
    line-height: var(--lh-relaxed);
    color: var(--text-default);
}

.home-overview__body p { margin: 0; }


/* Features bento */
.home-bento {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: var(--space-4);
}

.home-bento__card {
    grid-column: span 4;
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    padding: var(--space-5);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    box-shadow: var(--shadow-1);
    transition: box-shadow var(--duration-base) var(--ease-standard),
                border-color var(--duration-base) var(--ease-standard),
                transform var(--duration-base) var(--ease-standard);
}

.home-bento__card:hover {
    box-shadow: var(--shadow-2);
    border-color: var(--border-default);
    transform: translateY(-2px);
}

.home-bento__card--wide {
    grid-column: span 8;
    background: linear-gradient(135deg, var(--kmila-primary-dark) 0%, var(--kmila-primary) 100%);
    color: #ffffff;
    border-color: transparent;
}

.home-bento__card--wide h3 { color: #ffffff; }
.home-bento__card--wide p  { color: rgba(255, 255, 255, 0.85); }

.home-bento__card--highlight {
    grid-column: span 12;
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-5);
    align-items: center;
}

.home-bento__card h3 {
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    margin: 0;
    line-height: var(--lh-snug);
}

.home-bento__card p {
    margin: 0;
    color: var(--text-muted);
    line-height: var(--lh-relaxed);
    font-size: var(--fs-sm);
}

.home-bento__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: var(--radius-md);
    font-size: var(--fs-lg);
    margin-bottom: var(--space-2);
}

.home-bento__icon--accent  { background-color: var(--kmila-accent-soft); color: var(--kmila-accent); }
.home-bento__icon--primary { background-color: rgba(255, 255, 255, 0.15); color: #ffffff; }
.home-bento__icon--success { background-color: var(--success-soft);      color: var(--success); }
.home-bento__icon--warning { background-color: var(--warning-soft);      color: var(--warning); }

.home-bento__card--highlight .home-bento__icon--primary {
    background-color: var(--kmila-accent-soft);
    color: var(--kmila-accent);
    width: 56px;
    height: 56px;
    font-size: var(--fs-xl);
    margin-bottom: 0;
}

.home-bento__card--highlight h3 { font-size: var(--fs-lg); }


/* Closing CTA */
.home-cta {
    margin-top: var(--space-7);
    padding: var(--space-6) var(--space-5);
    background: linear-gradient(135deg, var(--kmila-primary-dark) 0%, var(--kmila-primary) 100%);
    color: #ffffff;
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-2);
}

.home-cta__inner {
    display: grid;
    grid-template-columns: 1.4fr 1fr;
    gap: var(--space-5);
    align-items: center;
}

.home-cta__text h2 {
    color: #ffffff;
    margin: 0 0 var(--space-2);
    font-size: var(--fs-xl);
    letter-spacing: var(--tracking-tight);
}

.home-cta__text p {
    color: rgba(255, 255, 255, 0.85);
    margin: 0;
    font-size: var(--fs-base);
    line-height: var(--lh-relaxed);
}

.home-cta__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    justify-content: flex-end;
    align-items: center;
}

/* Buttons inside the dark-gradient .home-cta panel must blend with that
   surface — override the kmila-btn fill to transparent and force light
   text (var(--surface-2) would read as a misplaced card on the gradient). */
.home-cta .kmila-btn {
    --kmila-btn-fill: transparent;
    color: rgba(255, 255, 255, 0.92);
}
.home-cta .kmila-btn::after { background-color: transparent; }
.home-cta .kmila-btn:hover,
.home-cta .kmila-btn:focus-visible { color: #ffffff; }


/* Home responsive */
@media (max-width: 991.98px) {
    .home-hero__grid {
        grid-template-columns: 1fr;
        gap: var(--space-4);
        text-align: center;
    }
    .home-hero__title,
    .home-hero__subtitle { max-width: none; }
    .home-hero__actions { justify-content: center; }
    .home-hero__art { order: -1; min-height: 180px; }
    .home-hero__logo { max-width: 160px; }

    .home-overview__body { grid-template-columns: 1fr; }

    .home-bento__card,
    .home-bento__card--wide,
    .home-bento__card--highlight { grid-column: span 6; }

    .home-bento__card--highlight { grid-template-columns: auto 1fr; }

    .home-cta__inner { grid-template-columns: 1fr; text-align: center; }
    .home-cta__actions { justify-content: center; }
}

@media (max-width: 575.98px) {
    .home-hero { padding: var(--space-5) var(--space-4); }
    .home-hero__logo { max-width: 130px; }

    .home-bento__card,
    .home-bento__card--wide,
    .home-bento__card--highlight { grid-column: span 12; }

    /* Single-column phones: cards stretch to full width and the surrounding
       hero / overview / CTA already center-align on this breakpoint, so a
       left-aligned bento card reads as a stranded paragraph. Match the rest
       of the page layout — center the text and the icon. */
    .home-bento__card {
        align-items: center;
        text-align: center;
    }
    .home-bento__card .home-bento__icon { margin-left: auto; margin-right: auto; }

    .home-bento__card--highlight { grid-template-columns: 1fr; text-align: center; }
    .home-bento__card--highlight .home-bento__icon--primary { margin: 0 auto var(--space-2); }

    .home-hero__primary { width: 100%; justify-content: center; }
}


/* ============================================================================
   Learn (/learn) — curriculum catalog
   ============================================================================ */

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

/* Header pattern matches Logs / Projects / Settings — plain block with a
   border-bottom underline. The old gradient + bordered card framing put the
   hero content 32 px further inset than every other route's header, which is
   why the page looked "padded differently" from the others. */
.learn-hero {
    display: grid;
    gap: var(--space-3);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
}

.learn-hero__title {
    font-size: var(--fs-2xl);
    font-weight: var(--fw-bold);
    letter-spacing: var(--tracking-tight);
    color: var(--text-strong);
    margin: 0;
}

.learn-hero__subtitle {
    color: var(--text-muted);
    font-size: var(--fs-md);
    max-width: 68ch;
    margin: 0;
}

.learn-hero__progress {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-top: var(--space-3);
    max-width: 520px;
}

.learn-hero__progress-track {
    position: relative;
    width: 100%;
    height: 10px;
    background-color: var(--surface-0);
    border-radius: 999px;
    overflow: hidden;
    border: 1px solid var(--border-subtle);
}

.learn-hero__progress-fill {
    position: absolute;
    inset: 0 auto 0 0;
    background: linear-gradient(90deg, var(--success) 0%, var(--kmila-accent) 100%);
    border-radius: 999px;
    transition: width var(--duration-slow) var(--ease-standard);
}

.learn-hero__progress-meta {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    font-size: var(--fs-sm);
}

.learn-hero__progress-count {
    font-family: var(--font-mono);
    font-weight: var(--fw-bold);
    color: var(--text-strong);
}

.learn-hero__progress-label {
    color: var(--text-muted);
}

/* ---------------------------------------------------------------------------
   Catalog filter chips (Learn + Practice).

   Layout: a vertical stack of horizontal chip rows. Each row holds a small
   inline label followed by a row of toggle chips. On narrow viewports
   (mobile) the chip row scrolls horizontally — hiding the scrollbar via
   `scrollbar-width: none` so the row reads as a single tappable strip.
   --------------------------------------------------------------------------- */
.catalog-filters {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-bottom: var(--space-5);
}

.catalog-filters__row {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: var(--space-2);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-block: var(--space-1);
    /* When chips overflow the row (mobile Spanish: "Avanzado" + "Experto"
       fall off the right edge), fade the trailing ~24 px so users see
       there's more content to scroll to. Same pattern as dock-tabs +
       editor-tabs. */
    -webkit-mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
            mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
}
[dir="rtl"] .catalog-filters__row {
    -webkit-mask-image: linear-gradient(to left, black calc(100% - 24px), transparent);
            mask-image: linear-gradient(to left, black calc(100% - 24px), transparent);
}
.catalog-filters__row::-webkit-scrollbar { display: none; }

.catalog-filters__label {
    flex: 0 0 auto;
    font-size: var(--fs-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
    margin-inline-end: var(--space-1);
}

/* Filter chips: visuals delegated to .kmila-btn-tab (Family 15). The
   local class only carries flex behavior so they don't shrink in the
   horizontally-scrolling chip row. Difficulty variants now drive the
   underline color via the --kmila-tab-color custom property instead of
   border-color so the Family 15 underline still encodes difficulty. */
.filter-chip { flex: 0 0 auto; }

.filter-chip--beginner     { --kmila-tab-color: var(--success); }
.filter-chip--easy         { --kmila-tab-color: var(--kmila-accent); }
.filter-chip--intermediate { --kmila-tab-color: var(--kmila-accent); }
.filter-chip--advanced     { --kmila-tab-color: var(--warning); }
.filter-chip--expert       { --kmila-tab-color: var(--danger); }

@media (max-width: 640px) {
    .catalog-filters__label { display: none; }
}

.learn-track {
    /* margin-bottom intentionally absent — `.learn-page` uses flex `gap` to
       space sibling tracks. */
}

.learn-track__header {
    margin-bottom: var(--space-4);
}

.learn-track__header h2 {
    font-size: var(--fs-xl);
    font-weight: var(--fw-bold);
    color: var(--text-strong);
    margin: var(--space-1) 0;
}

.learn-track__header p {
    color: var(--text-muted);
    margin: 0;
    max-width: 72ch;
}

.learn-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: var(--space-4);
}

.learn-card {
    position: relative;
    display: grid;
    grid-template-rows: auto 1fr auto;
    gap: var(--space-3);
    padding: var(--space-4);
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    text-decoration: none;
    color: inherit;
    overflow: hidden;
    transition: transform var(--duration-fast) var(--ease-standard),
                box-shadow var(--duration-fast) var(--ease-standard),
                border-color var(--duration-fast) var(--ease-standard);
}

.learn-card:hover,
.learn-card:focus-visible {
    transform: translateY(-2px);
    border-color: var(--kmila-accent);
    box-shadow: var(--shadow-2);
    text-decoration: none;
    color: inherit;
}

.learn-card__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    border-radius: var(--radius-md);
    font-size: var(--fs-lg);
}

.learn-card__icon--success { background-color: rgba(16, 185, 129, 0.12); color: var(--success); }
.learn-card__icon--primary { background-color: color-mix(in srgb, var(--kmila-accent) 12%, transparent); color: var(--kmila-accent); }
.learn-card__icon--info    { background-color: rgba(14, 165, 233, 0.12); color: var(--info); }
.learn-card__icon--warning { background-color: rgba(245, 158, 11, 0.14); color: var(--warning); }
.learn-card__icon--danger  { background-color: rgba(239, 68, 68, 0.12);  color: var(--danger); }

.learn-card__title {
    font-size: var(--fs-md);
    font-weight: var(--fw-bold);
    color: var(--text-strong);
    margin: 0 0 var(--space-1);
    line-height: 1.3;
}

.learn-card__summary {
    color: var(--text-muted);
    font-size: var(--fs-sm);
    line-height: 1.5;
    margin: 0;
}

.learn-card__meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    font-size: var(--fs-xs);
    color: var(--text-muted);
}

.learn-card__count {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    font-family: var(--font-mono);
}

.learn-card__chip {
    display: inline-flex;
    align-items: center;
    padding: 2px var(--space-2);
    border-radius: 999px;
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-bold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
}

.learn-card__chip--beginner     { background: rgba(16, 185, 129, 0.15); color: var(--success); }
.learn-card__chip--easy         { background: color-mix(in srgb, var(--kmila-accent) 15%, transparent); color: var(--kmila-accent); }
.learn-card__chip--intermediate { background: rgba(14, 165, 233, 0.15); color: var(--info); }
.learn-card__chip--advanced     { background: rgba(245, 158, 11, 0.15); color: var(--warning); }
.learn-card__chip--expert       { background: rgba(239, 68, 68, 0.15);  color: var(--danger); }

.learn-card__progress-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 2px var(--space-2);
    border-radius: 999px;
    background-color: rgba(16, 185, 129, 0.12);
    color: var(--success);
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-bold);
}

.learn-card__bar {
    position: absolute;
    inset: auto 0 0 0;
    height: 3px;
    background-color: var(--surface-0);
}
.learn-card__bar-fill {
    height: 100%;
    background: linear-gradient(90deg, var(--success) 0%, var(--kmila-accent) 100%);
    transition: width var(--duration-slow) var(--ease-standard);
}

.learn-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    padding: var(--space-6);
    text-align: center;
    background-color: var(--surface-1);
    border: 1px dashed var(--border-subtle);
    border-radius: var(--radius-lg);
    color: var(--text-muted);
}

.learn-empty__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background-color: color-mix(in srgb, var(--kmila-accent) 12%, transparent);
    color: var(--kmila-accent);
    font-size: var(--fs-xl);
}


/* ============================================================================
   Lesson (/learn/{module}/{lesson}) — single lesson view
   ============================================================================ */

.lesson-page {
    display: grid;
    gap: var(--space-5);
    /* Force the implicit single-column track to ignore intrinsic content
       width so wide children (long <pre> ASCII art, mermaid SVGs, wide
       tables) don't blow up the column and clip the prose `<p>`s. */
    grid-template-columns: minmax(0, 1fr);
}

/* Backlog #12: chrome, players (ExecutionPlayer / GateAnimator /
   FsmAnimator), Blockly board, sample CTA, and footer span the full
   content area like every other route. Only the prose elements below
   re-impose a ~72ch reading column so long sentences stay legible on
   wide monitors. */
.lesson-markdown,
.lesson-body > h1,
.lesson-page__subtitle,
.lesson-fallback-banner {
    /* `min(72ch, 100%)` keeps the desktop reading column at ~72ch while
       guaranteeing we never overflow a narrow mobile viewport (where
       72ch can be 2× the device width). */
    max-width: min(72ch, 100%);
    margin-inline: auto;
}

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

.lesson-crumb {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    color: var(--text-muted);
    font-size: var(--fs-sm);
    text-decoration: none;
    width: fit-content;
}

.lesson-crumb:hover {
    color: var(--kmila-accent);
    text-decoration: none;
}

.lesson-chip-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

.lesson-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 3px var(--space-2);
    border-radius: 999px;
    font-size: var(--fs-2xs);
    font-family: var(--font-mono);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    background-color: var(--surface-1);
    color: var(--text-muted);
    border: 1px solid var(--border-subtle);
}

.lesson-chip--kind { color: var(--kmila-accent); border-color: color-mix(in srgb, var(--kmila-accent) 30%, transparent); }
.lesson-chip--time { color: var(--text-muted); }
.lesson-chip--done {
    color: var(--success);
    border-color: rgba(16, 185, 129, 0.3);
    background-color: rgba(16, 185, 129, 0.10);
}

.lesson-body {
    display: grid;
    gap: var(--space-4);
    min-width: 0;
    grid-template-columns: minmax(0, 1fr);
}
.lesson-body > * { min-width: 0; }

.lesson-markdown {
    color: var(--text-body);
    line-height: 1.7;
    min-width: 0;
    overflow-wrap: anywhere;
}
.lesson-markdown h1 {
    font-size: clamp(1.6rem, 1.3rem + 1.2vw, 2.2rem);
    font-weight: var(--fw-bold);
    color: var(--text-strong);
    margin: 0 0 var(--space-4);
    letter-spacing: var(--tracking-tight);
    border-bottom: 1px solid var(--border-subtle);
    padding-bottom: var(--space-3);
    /* Long lesson titles ("Number systems: bits, hex, two's complement")
       wrap to the next line on narrow mobile viewports instead of clipping
       past the card edge. `overflow-wrap` doesn't inherit so it has to be
       declared on the leaf element. */
    overflow-wrap: anywhere;
}
.lesson-markdown h2 {
    font-size: var(--fs-xl);
    font-weight: var(--fw-bold);
    color: var(--text-strong);
    margin: var(--space-5) 0 var(--space-2);
}
.lesson-markdown h3 {
    font-size: var(--fs-lg);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    margin: var(--space-4) 0 var(--space-2);
}
.lesson-markdown p, .lesson-markdown ul, .lesson-markdown ol {
    margin: 0 0 var(--space-3);
}
.lesson-markdown ul, .lesson-markdown ol {
    padding-left: 1.4rem;
}
.lesson-markdown li {
    margin-bottom: var(--space-1);
}
.lesson-markdown code {
    font-family: var(--font-mono);
    font-size: 0.92em;
    padding: 2px 6px;
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    color: var(--kmila-accent);
}
.lesson-markdown pre {
    padding: var(--space-3);
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    overflow-x: auto;
    margin: var(--space-3) 0;
    /* Preserve tabs, spaces, and line breaks exactly — critical for
       ASCII-art diagrams (box-drawing, truth tables, pipelined-CPU
       schematics) that rely on column alignment. Also force identifiers
       to stay whole (no mid-token wrap like `STD_LOGIC_1164.ALL;` → next
       line) by pinning the break-policy explicitly — the lesson card's
       narrower mobile column otherwise inherits anywhere-wrap rules. */
    white-space: pre;
    word-break: keep-all;
    overflow-wrap: normal;
    tab-size: 4;
}
.lesson-markdown pre code,
.lesson-markdown pre code * {
    /* Re-assert inside the syntax-highlighted spans that kmilaColorizeLessonCode
       injects — without this the highlighter's wrappers re-enable word-break
       from inherited container styles. */
    word-break: keep-all;
    overflow-wrap: normal;
    white-space: pre;
}
.lesson-markdown pre code {
    background: transparent;
    border: none;
    padding: 0;
    color: var(--text-body);
    font-size: var(--fs-sm);
    line-height: 1.55;
    font-family: var(--font-mono);
    /* Turn off stylistic ligatures/contextual alternates — they break the
       apparent width of box-drawing characters in JetBrains Mono. */
    font-variant-ligatures: none;
    font-feature-settings: "liga" 0, "calt" 0;
}
.lesson-markdown blockquote {
    margin: var(--space-3) 0;
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-1);
    border-left: 3px solid var(--kmila-accent);
    border-radius: 0 var(--radius-md) var(--radius-md) 0;
    color: var(--text-muted);
}
.lesson-markdown table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    margin: var(--space-4) 0;
    font-size: var(--fs-sm);
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    overflow: hidden;
    box-shadow: var(--shadow-1);
}
.lesson-markdown th, .lesson-markdown td {
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
    text-align: left;
    vertical-align: top;
}
.lesson-markdown tr:last-child td { border-bottom: none; }
.lesson-markdown tbody tr:nth-child(even) td {
    background-color: var(--surface-2);
}
.lesson-markdown td code,
.lesson-markdown th code {
    /* Table cells usually contain opcodes, hex literals, or signal names —
       emphasise monospace and keep the colour consistent with surrounding. */
    font-size: 0.95em;
}
.lesson-markdown th {
    background-color: var(--surface-2);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
}

.lesson-sample-cta {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-4);
    background: linear-gradient(135deg, color-mix(in srgb, var(--kmila-accent) 8%, transparent) 0%, var(--surface-1) 100%);
    border: 1px solid color-mix(in srgb, var(--kmila-accent) 28%, transparent);
    border-radius: var(--radius-lg);
}

.lesson-sample-cta__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: var(--radius-md);
    background-color: color-mix(in srgb, var(--kmila-accent) 15%, transparent);
    color: var(--kmila-accent);
    font-size: var(--fs-lg);
}

.lesson-sample-cta__body h3 {
    font-size: var(--fs-md);
    font-weight: var(--fw-bold);
    color: var(--text-strong);
    margin: 0 0 var(--space-1);
}

.lesson-sample-cta__body p {
    color: var(--text-muted);
    font-size: var(--fs-sm);
    margin: 0;
}

.lesson-sample-cta__btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    white-space: nowrap;
}

.lesson-missing {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3);
    background-color: rgba(245, 158, 11, 0.1);
    color: var(--warning);
    border: 1px solid rgba(245, 158, 11, 0.3);
    border-radius: var(--radius-md);
    font-size: var(--fs-sm);
}

.lesson-fallback-banner {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-4);
    padding: var(--space-2) var(--space-3);
    background-color: var(--kmila-accent-soft);
    color: var(--kmila-accent);
    border: 1px solid var(--kmila-accent);
    border-radius: var(--radius-md);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
}

.lesson-footer {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border-subtle);
}

.lesson-footer__nav {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-3);
}

.lesson-nav-btn {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3);
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    color: inherit;
    text-decoration: none;
    transition: border-color var(--duration-fast) var(--ease-standard);
}

.lesson-nav-btn:hover {
    border-color: var(--kmila-accent);
    color: inherit;
    text-decoration: none;
}

.lesson-nav-btn small {
    display: block;
    color: var(--text-muted);
    font-size: var(--fs-2xs);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
}

.lesson-nav-btn span {
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    font-size: var(--fs-sm);
}

.lesson-nav-btn--next { justify-content: flex-end; text-align: right; }
.lesson-nav-btn--disabled { visibility: hidden; }

@media (max-width: 640px) {
    .lesson-sample-cta {
        grid-template-columns: 1fr;
        text-align: center;
    }
    .lesson-sample-cta__icon { margin: 0 auto; }
    .lesson-sample-cta__btn { justify-self: stretch; justify-content: center; }

    .lesson-footer__nav { grid-template-columns: 1fr; }
}


/* Run-bar: always-visible Save / New-file actions next to the overflow dots.
   Hidden on narrow viewports to keep Build + Run reachable — users still have
   the overflow menu and the Ctrl+S / Cmd+S shortcut. */
.run-bar__file-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: 0 var(--space-3);
    color: var(--text-muted);
    white-space: nowrap;
}

.run-bar__file-btn:hover {
    color: var(--text-strong);
    background-color: var(--surface-2);
}

.run-bar__file-btn-label {
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
}

@media (max-width: 900px) {
    .run-bar__file-btn-label { display: none; }
}

@media (max-width: 640px) {
    /* Keep only Build + Run visible; rely on overflow for Save / New-file. */
    .run-bar__file-btn { display: none; }
}


/* Make the FPGA chip behave like a button — pointer cursor + hover surface
   shift — now that clicking it opens the TopBar FPGA picker. */
.run-bar__fpga {
    cursor: pointer;
    border: 1px solid var(--border-subtle);
    background: var(--surface-2);
    transition: background-color var(--duration-fast) var(--ease-standard),
                border-color var(--duration-fast) var(--ease-standard);
}

.run-bar__fpga:hover {
    background: var(--surface-3);
    border-color: var(--border-strong, var(--border-subtle));
}

.run-bar__fpga-chevron {
    margin-left: 4px;
    opacity: 0.6;
    font-size: 0.75em;
}

/* Runs panel toolbar — sits above the list, hosts the Import action. */
.runs-panel__toolbar {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
}

.runs-panel__toolbar-spacer { flex: 1 1 auto; }

.runs-panel__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 22px;
    height: 20px;
    padding: 0 8px;
    background-color: var(--surface-3);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    border-radius: var(--radius-pill);
}

/* The Import "button" is actually a label wrapping a hidden InputFile so the
   browser's native picker opens on click. We re-style it to look identical to
   a Bootstrap btn-ghost btn-sm. */
.runs-panel__import {
    cursor: pointer;
    margin: 0;
}

.runs-panel__import:hover {
    background-color: var(--surface-2);
}

/* Run-bar: flagged FPGA chip when nothing is selected. Uses the danger tone
   so the "this is why Build/Run are disabled" signal is unmistakable. */
.run-bar__fpga--missing {
    color: var(--danger);
    background-color: rgba(239, 68, 68, 0.08);
    border-color: rgba(239, 68, 68, 0.4);
}

.run-bar__fpga--missing .run-bar__fpga-empty {
    color: var(--danger);
    font-weight: var(--fw-semibold);
}

/* Inline no-FPGA hint chip inside .run-bar__actions. Sits next to Build so
   the reason for the greyed action is visible on touch (title tooltips don't
   fire on touch). Reuses the danger tone from .run-bar__fpga--missing. */
.run-bar__no-fpga-hint {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-pill);
    border: 1px solid rgba(239, 68, 68, 0.4);
    background-color: rgba(239, 68, 68, 0.08);
    color: var(--danger);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    line-height: 1;
    cursor: pointer;
    white-space: nowrap;
}
.run-bar__no-fpga-hint:hover,
.run-bar__no-fpga-hint:focus-visible {
    background-color: rgba(239, 68, 68, 0.14);
    outline: none;
}
/* At narrow container widths the label is dropped; only the icon remains so
   the chip stays out of the way of Build/Run/Stop labels. The container is
   named `runbar` in `.run-bar` (no hyphen) — the old `@container run-bar`
   silently no-op'd, letting the long RUNBAR_FPGA_REQUIRED sentence overflow
   the row and push Run/Stop/burger off-screen on phones (real-device audit
   2026-08-02). */
@container runbar (max-width: 640px) {
    .run-bar__no-fpga-hint-label { display: none; }
}


/* Settings: custom FPGA + language cards */
.settings-card--wide { grid-column: 1 / -1; }

/* InputFile is invisible; its <label> is styled as the actual button and
   receives the click that opens the OS file picker. */
.settings-import-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    cursor: pointer;
    margin: 0;
}
.settings-import-btn .d-none { display: none !important; }

.settings-list {
    list-style: none;
    margin: var(--space-3) 0 0;
    padding: 0;
    display: grid;
    gap: var(--space-1);
}

.settings-list__item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
}

.settings-list__info {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}

.settings-list__name {
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.settings-list__code {
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    color: var(--text-muted);
    background-color: var(--surface-2);
    padding: 1px var(--space-2);
    border-radius: 999px;
}

/* ─── Account / GitHub login (Settings) ─────────────────────────────
   Almost everything in the Account card is now expressed in the
   page's existing primitives — .settings-list__item for the profile
   row, .settings-row for the toggle / form input pair,
   .settings-card__actions for the buttons, .settings-card__note for
   the privacy declaration. The styles below are the small additions
   that don't fit those primitives.
*/

.settings-account__avatar {
    border-radius: 50%;
    flex-shrink: 0;
    object-fit: cover;
}

/* Numbered setup steps. Same font / colour / spacing as a regular
   .settings-card__hint paragraph — the <ol> just adds the visible
   numbering. */
.settings-account__setup-steps {
    margin: 0 0 var(--space-2);
    padding-left: var(--space-5);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    color: var(--text-default);
    font-size: var(--fs-sm);
}

.settings-account__setup-steps a {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    margin-left: var(--space-1);
    color: var(--kmila-accent);
    font-family: var(--font-mono);
    text-decoration: none;
}

.settings-account__setup-steps a:hover { text-decoration: underline; }

/* Inline <details> disclosure for the "Change OAuth Client ID"
   secondary action. Mirrors the .kmila-fpga-picker pattern — same
   ChevronDown rotation on [open], same hidden ::marker, same
   muted-summary visual rest state. */
.settings-card__details {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px dashed var(--border-subtle);
}

.settings-card__details > summary {
    cursor: pointer;
    list-style: none;
    user-select: none;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--text-muted);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
}

.settings-card__details > summary::-webkit-details-marker { display: none; }
.settings-card__details > summary::marker             { display: none; content: ""; }

.settings-card__details > summary:hover {
    color: var(--text-default);
}

.settings-card__details-chevron {
    margin-left: var(--space-1);
    transition: transform var(--duration-fast) var(--ease-standard);
}

.settings-card__details[open] .settings-card__details-chevron {
    transform: rotate(180deg);
}

.settings-card__details > .settings-row,
.settings-card__details > .settings-card__actions {
    margin-top: var(--space-3);
}

/* TopBar account pill — small persistent chip mirroring the FPGA picker style.
   Uses the [open]-gated panel pattern from .kmila-fpga-picker so the popover
   stays hidden until the user clicks; otherwise the panel can render visibly
   below the summary even with <details> closed (Safari is particularly lax).
*/
.kmila-account-pill {
    position: relative;
}

.kmila-account-pill[open] > .kmila-account-pill__panel {
    display: flex;
}

.kmila-account-pill__summary {
    list-style: none;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    color: var(--text-default);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    line-height: 1.2;
    max-width: 220px;
    user-select: none;
    transition: background-color var(--duration-fast) var(--ease-standard),
                border-color var(--duration-fast) var(--ease-standard);
}

/* Hide the native disclosure triangle in every engine: WebKit-prefixed
   marker for Safari, generic ::marker for Firefox + Chromium. */
.kmila-account-pill__summary::-webkit-details-marker { display: none; }
.kmila-account-pill__summary::marker             { display: none; content: ""; }

.kmila-account-pill__summary:hover {
    background-color: var(--surface-2);
    border-color: var(--border-default);
}

.kmila-account-pill[open] .kmila-account-pill__summary {
    border-color: var(--kmila-accent);
    box-shadow: var(--shadow-focus);
}

.kmila-account-pill__avatar {
    border-radius: 50%;
    flex-shrink: 0;
    width: 24px;
    height: 24px;
    object-fit: cover;
}

.kmila-account-pill__avatar--large {
    width: 40px;
    height: 40px;
    border: 2px solid var(--border-subtle);
}

.kmila-account-pill__name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 140px;
    font-weight: var(--fw-semibold);
}

/* Panel — hidden by default; [open] flips it to flex above. */
.kmila-account-pill__panel {
    display: none;
    position: absolute;
    top: calc(100% + var(--space-2));
    right: 0;
    left: auto;
    z-index: 100;
    width: min(320px, 90vw);
    padding: var(--space-3);
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
    flex-direction: column;
    gap: var(--space-2);
}

[dir="rtl"] .kmila-account-pill__panel {
    right: auto;
    left: 0;
}

.kmila-account-pill__head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border-subtle);
}

.kmila-account-pill__head-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.kmila-account-pill__head-text strong { color: var(--text-strong); }

.kmila-account-pill__head-text small {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
}

.kmila-account-pill__action {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background: transparent;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    color: var(--text-default);
    font-size: var(--fs-sm);
    cursor: pointer;
    text-align: left;
}

.kmila-account-pill__action:hover { background-color: var(--surface-1); }

.kmila-account-pill__action--primary {
    background-color: var(--kmila-accent);
    border-color: var(--kmila-accent);
    color: #ffffff;
}

.kmila-account-pill__action--primary:hover {
    background-color: var(--kmila-accent);
    filter: brightness(1.05);
}

.kmila-account-pill__action--danger {
    color: var(--danger);
}

.kmila-account-pill__hint,
.kmila-account-pill__privacy {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--fs-xs);
    line-height: var(--lh-snug);
}

.kmila-account-pill__privacy {
    display: flex;
    align-items: flex-start;
    gap: var(--space-1);
    padding-top: var(--space-2);
    border-top: 1px solid var(--border-subtle);
}

/* ============================================================================
   ProfileMenu — consolidated TopBar dropdown that replaced the four separate
   pills (account, FPGA, language, theme). Inherits the visual language of
   .kmila-account-pill but with a wider panel that hosts grouped sections.
   ============================================================================ */

.kmila-profile-menu {
    position: relative;
}

.kmila-profile-menu[open] > .kmila-profile-menu__panel {
    display: flex;
}

.kmila-profile-menu__summary {
    list-style: none;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: 4px 8px 4px 4px;
    background-color: var(--surface-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    color: var(--text-default);
    font-size: var(--fs-sm);
    line-height: 1.2;
    user-select: none;
    transition: background-color var(--duration-fast) var(--ease-standard),
                border-color var(--duration-fast) var(--ease-standard);
}

.kmila-profile-menu__summary::-webkit-details-marker { display: none; }
.kmila-profile-menu__summary::marker             { display: none; content: ""; }

.kmila-profile-menu__summary:hover {
    background-color: var(--surface-2);
    border-color: var(--border-default);
}

.kmila-profile-menu[open] > .kmila-profile-menu__summary {
    border-color: var(--kmila-accent);
    box-shadow: var(--shadow-focus);
}

.kmila-profile-menu__avatar {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    flex-shrink: 0;
    object-fit: cover;
}

.kmila-profile-menu__avatar--large {
    width: 44px;
    height: 44px;
    border: 2px solid var(--border-subtle);
}

/* Generic-person fallback when there's no GitHub avatar. */
.kmila-profile-menu__avatar--placeholder {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background-color: var(--surface-3);
    color: var(--text-muted);
    border: 1px solid var(--border-subtle);
}

.kmila-profile-menu__chevron {
    color: var(--text-muted);
    font-size: 0.75em;
}

/* Panel — hidden by default; [open] flips it to flex above. Wider than the
   old account-pill panel since it hosts FPGA + language + theme + actions. */
.kmila-profile-menu__panel {
    display: none;
    position: absolute;
    top: calc(100% + var(--space-2));
    right: 0;
    left: auto;
    z-index: var(--z-popover);
    width: min(360px, 92vw);
    padding: var(--space-3);
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
    flex-direction: column;
    gap: var(--space-3);
    max-height: min(80vh, 720px);
    overflow-y: auto;
}

[dir="rtl"] .kmila-profile-menu__panel {
    right: auto;
    left: 0;
}

.kmila-profile-menu__head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
}

.kmila-profile-menu__head-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.kmila-profile-menu__head-text strong { color: var(--text-strong); }

.kmila-profile-menu__head-text small {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
}

/* Section block — shared by FPGA / language / theme / actions. */
.kmila-profile-menu__section {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding-top: var(--space-2);
    border-top: 1px solid transparent;
}

.kmila-profile-menu__section + .kmila-profile-menu__section {
    border-top-color: var(--border-subtle);
}

.kmila-profile-menu__section--actions {
    gap: 4px;
}

.kmila-profile-menu__section-head {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--text-muted);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Inline FPGA picker — same .kmila-fpga-picker rules apply but the inline
   variant fits inside the profile panel (no fixed positioning). */
.kmila-fpga-picker--inline { position: relative; }

.kmila-fpga-picker__panel--inline {
    position: static;
    width: 100%;
    margin-top: var(--space-2);
    box-shadow: none;
    border: 1px solid var(--border-subtle);
}

/* Language grid — 3-or-4 column layout depending on width. */
.kmila-profile-menu__lang-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
    gap: 4px;
}

/* Lang chips: visuals delegated to .kmila-btn-tab. The local class only
   keeps the column layout (code on top, native name below) since the
   default .kmila-btn-tab is row-based. */
.kmila-profile-menu__lang-chip {
    flex-direction: column;
    gap: 2px;
    padding: 6px 4px;
    justify-content: center;
}

.kmila-profile-menu__lang-code {
    font-family: var(--font-mono);
    font-weight: var(--fw-bold);
    font-size: var(--fs-xs);
    letter-spacing: 0.04em;
}

.kmila-profile-menu__lang-native {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.kmila-profile-menu__lang-chip.is-active .kmila-profile-menu__lang-native {
    color: var(--text-default);
}

/* Theme — 3 buttons in a row, evenly spaced. */
.kmila-profile-menu__theme-row {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 4px;
}

/* Theme buttons: visuals delegated to .kmila-btn-tab. Local class only
   carries the icon-on-top column layout. */
.kmila-profile-menu__theme-btn {
    flex-direction: column;
    gap: 4px;
    padding: 8px 4px;
    justify-content: center;
}

/* Action rows — Settings, Sign in, Sign out. Visuals from .kmila-btn;
   local class only ensures the buttons stretch to fill the action
   section's vertical column (the .kmila-btn default is inline-flex). */
.kmila-profile-menu__action { width: 100%; justify-content: flex-start; }


/* Device-flow modal layout. */
.github-login__intro {
    margin: 0 0 var(--space-3);
    color: var(--text-muted);
}

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

.github-login__step {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3);
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
}

.github-login__step-num {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    background-color: var(--kmila-accent);
    color: #ffffff;
    border-radius: 50%;
    font-weight: var(--fw-bold);
}

.github-login__step-title {
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    margin-bottom: var(--space-1);
}

.github-login__verification-link {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    color: var(--kmila-accent);
}

.github-login__code-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.github-login__code {
    font-family: var(--font-mono);
    font-size: var(--fs-xl);
    font-weight: var(--fw-bold);
    letter-spacing: var(--tracking-wide);
    padding: var(--space-1) var(--space-3);
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    color: var(--text-strong);
    user-select: all;
}

.github-login__waiting {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--text-muted);
    font-size: var(--fs-sm);
}

.github-login__loading {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-4);
    color: var(--text-muted);
}

.github-login__privacy {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    margin: var(--space-3) 0 0;
    padding: var(--space-2) var(--space-3);
    background-color: color-mix(in srgb, var(--kmila-accent) 6%, transparent);
    border: 1px dashed color-mix(in srgb, var(--kmila-accent) 30%, transparent);
    border-radius: var(--radius-md);
    color: var(--text-muted);
    font-size: var(--fs-xs);
}


.settings-card__note {
    display: flex;
    gap: var(--space-2);
    align-items: flex-start;
    margin: var(--space-3) 0 0;
    padding: var(--space-2) var(--space-3);
    background-color: color-mix(in srgb, var(--kmila-accent) 6%, transparent);
    border: 1px dashed color-mix(in srgb, var(--kmila-accent) 30%, transparent);
    border-radius: var(--radius-md);
    color: var(--text-muted);
    font-size: var(--fs-sm);
}


/* ----------------------------------------------------------------------------
   Sidebar project rows — hover actions + "view all" link
   ---------------------------------------------------------------------------- */

/* The project row is now a wrapper div that contains the nav link AND an
   action strip on the right. The existing .kmila-nav-item styles still apply
   to the wrapper; we just pad/flex the inner parts. */
.kmila-nav-item--project {
    padding: 0;             /* the link below owns its padding */
    position: relative;
}

.kmila-nav-item__main {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-1) var(--space-4);
    min-height: 36px;
    color: inherit;
    text-decoration: none;
    min-width: 0;
    border-radius: var(--radius-sm);
}

.kmila-nav-item__main:hover,
.kmila-nav-item__main:focus-visible {
    color: inherit;
    text-decoration: none;
}

.kmila-nav-item__actions {
    position: absolute;
    right: var(--space-2);
    top: 50%;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--duration-fast) var(--ease-standard);
}

[dir="rtl"] .kmila-nav-item__actions {
    right: auto;
    left: var(--space-2);
}

.kmila-nav-item--project:hover .kmila-nav-item__actions,
.kmila-nav-item--project:focus-within .kmila-nav-item__actions {
    opacity: 1;
    pointer-events: auto;
}

.kmila-nav-item__action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    border: none;
    border-radius: var(--radius-xs);
    background-color: rgba(255, 255, 255, 0.08);
    color: rgba(255, 255, 255, 0.85);
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-standard),
                color var(--duration-fast) var(--ease-standard);
}

.kmila-nav-item__action:hover {
    background-color: rgba(239, 68, 68, 0.35);
    color: #ffffff;
}

.kmila-nav-item__action:focus-visible {
    outline: 2px solid var(--kmila-accent);
    outline-offset: 1px;
}

/* Hide hover actions in collapsed (rail) mode — no room, no point. */
.kmila-sidebar--collapsed .kmila-nav-item__actions { display: none; }

/* "View all 42 projects" link at the bottom of the sidebar project section. */
.kmila-sidebar__view-all {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: var(--space-2) var(--space-3) 0;
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-sm);
    color: rgba(255, 255, 255, 0.6);
    font-size: var(--fs-xs);
    font-weight: var(--fw-medium);
    text-decoration: none;
    border: 1px dashed rgba(255, 255, 255, 0.15);
    transition: color var(--duration-fast) var(--ease-standard),
                border-color var(--duration-fast) var(--ease-standard);
}

.kmila-sidebar__view-all:hover {
    color: #ffffff;
    text-decoration: none;
    border-color: rgba(255, 255, 255, 0.35);
}

.kmila-sidebar--collapsed .kmila-sidebar__view-all span { display: none; }


/* Learn hero: "continue where you left off" CTA banner */
.learn-hero__continue {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    margin-top: var(--space-4);
    padding: var(--space-2) var(--space-4);
    background: linear-gradient(135deg, color-mix(in srgb, var(--kmila-accent) 12%, transparent) 0%, var(--surface-2) 100%);
    border: 1px solid color-mix(in srgb, var(--kmila-accent) 30%, transparent);
    border-radius: var(--radius-lg);
    color: inherit;
    text-decoration: none;
    box-shadow: var(--shadow-1);
    transition: transform var(--duration-fast) var(--ease-standard),
                border-color var(--duration-fast) var(--ease-standard);
}

.learn-hero__continue:hover {
    transform: translateY(-1px);
    border-color: var(--kmila-accent);
    color: inherit;
    text-decoration: none;
}

.learn-hero__continue-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--kmila-accent);
    font-size: var(--fs-xl);
    flex-shrink: 0;
}

.learn-hero__continue-body {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.learn-hero__continue-body small {
    color: var(--text-muted);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
}

.learn-hero__continue-body strong {
    color: var(--text-strong);
    font-size: var(--fs-md);
    font-weight: var(--fw-bold);
    line-height: 1.3;
}

.learn-hero__continue-module {
    font-size: var(--fs-xs);
    color: var(--text-muted);
    margin-top: 2px;
}

.learn-hero__continue-chevron {
    color: var(--kmila-accent);
    margin-left: var(--space-3);
    flex-shrink: 0;
}

[dir="rtl"] .learn-hero__continue-chevron {
    margin-left: 0;
    margin-right: var(--space-3);
    transform: scaleX(-1);
}


/* ----------------------------------------------------------------------------
   Toasts — token-aligned reskin of Bootstrap's .toast
   ----------------------------------------------------------------------------
   BlazorBootstrap's Toasts component ships the default Bootstrap chrome, which
   looks dated next to the rest of the redesigned UI. Reskinning rather than
   rewriting keeps the component's ARIA + auto-hide + placement logic intact.
*/

/* ----------------------------------------------------------------------------
   KmilaToast — custom glass-styled toast stack (replaces BlazorBootstrap Toasts)
   ----------------------------------------------------------------------------
   Backed by `Services/KmilaToastService.cs` + `Components/KmilaToastHost.razor`.
   The old `.toast`/`.toast-container` block below is kept for any leftover BB
   usage (modals/dialogs still rely on it indirectly) but the live host paints
   through `.kmila-toast-host` + `.kmila-toast` exclusively.
*/

.kmila-toast-host {
    position: fixed;
    /* Toast is top-anchored on every viewport, clearing the TopBar and
       the iOS safe-area notch. Previous bottom-anchored phone override
       (B-1 round-2 v3, 2026-06-29) was reverted per user request
       2026-07-05: bottom placement was obscured by the sticky RunBar /
       dock and easy to miss; top placement surfaces above every editor
       chrome layer. */
    top: calc(var(--topbar-height, 56px) + env(safe-area-inset-top, 0px) + var(--space-2));
    left: 50%;
    transform: translateX(-50%);
    z-index: var(--z-toast);
    width: min(440px, calc(100vw - var(--space-4) * 2));
    padding: var(--space-3);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    pointer-events: none;       /* let clicks through gaps between toasts */
}

@media (max-width: 768px) {
    .kmila-toast-host {
        /* Tighter card on phone so it never spans the full viewport. */
        width: min(360px, calc(100vw - var(--space-3) * 2));
    }
}
.kmila-toast-host > * { pointer-events: auto; }

.kmila-toast {
    --t-accent: var(--kmila-accent);
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);
    background: color-mix(in srgb, var(--surface-2) 70%, transparent);
    -webkit-backdrop-filter: blur(16px) saturate(1.6);
    backdrop-filter: blur(16px) saturate(1.6);
    border: 1px solid color-mix(in srgb, var(--t-accent) 35%, var(--border-subtle));
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-3),
                inset 0 0 0 1px color-mix(in srgb, var(--t-accent) 8%, transparent),
                0 0 30px -10px var(--t-accent);
    color: var(--text-strong);
    font-size: var(--fs-sm);
    animation: kmila-toast-in var(--duration-base) var(--ease-standard) both;
}

/* Fallback for browsers without backdrop-filter (covers older WebViews). */
@supports not ((backdrop-filter: blur(16px)) or (-webkit-backdrop-filter: blur(16px))) {
    .kmila-toast { background: var(--surface-2); }
}

.kmila-toast--success { --t-accent: var(--success); }
.kmila-toast--danger  { --t-accent: var(--danger); }
.kmila-toast--warning { --t-accent: var(--warning); }
.kmila-toast--info    { --t-accent: var(--kmila-accent); }

.kmila-toast__icon {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
    color: var(--t-accent);
    margin-top: 2px;
}

.kmila-toast__text {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.kmila-toast__title {
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    letter-spacing: var(--tracking-tight);
}

.kmila-toast__body {
    font-size: var(--fs-sm);
    color: var(--text-default);
    line-height: 1.45;
    overflow-wrap: anywhere;
    word-break: break-word;
}

.kmila-toast__close {
    appearance: none;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    padding: 0;
    cursor: pointer;
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    border-radius: var(--radius-sm);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: color var(--duration-base) var(--ease-standard),
                background-color var(--duration-base) var(--ease-standard);
}
.kmila-toast__close:hover { color: var(--text-strong); background: rgba(148, 163, 184, 0.1); }
.kmila-toast__close:focus-visible {
    outline: none;
    box-shadow: var(--shadow-focus);
}
.kmila-toast__close svg { width: 16px; height: 16px; }

@keyframes kmila-toast-in {
    from { opacity: 0; transform: translateY(-12px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* Phones: respect the iOS notch + cap width to fit narrow viewports. The
   modal block applies the same pattern; keep the values in sync. */
@media (max-width: 640.98px) {
    .kmila-toast-host {
        width: calc(100vw - var(--space-2));
        padding-top: calc(var(--space-3) + env(safe-area-inset-top, 0px));
        padding-left: calc(var(--space-3) + env(safe-area-inset-left, 0px));
        padding-right: calc(var(--space-3) + env(safe-area-inset-right, 0px));
    }
}


/* ----------------------------------------------------------------------------
   Legacy BlazorBootstrap toast reskin (kept until BB Toasts is fully removed).
   ---------------------------------------------------------------------------- */

.toast-container {
    padding: var(--space-3);
    pointer-events: none;               /* container doesn't intercept clicks */
    z-index: var(--z-toast);
}
.toast-container .toast {
    pointer-events: auto;               /* but individual toasts do */
}

.toast {
    min-width: 300px;
    max-width: 420px;
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-2);
    color: var(--text-body);
    font-size: var(--fs-sm);
    overflow: hidden;
    backdrop-filter: saturate(1.2);
}

.toast-header {
    background-color: transparent;
    border-bottom: 1px solid var(--border-subtle);
    padding: var(--space-2) var(--space-3);
    color: var(--text-strong);
    font-weight: var(--fw-semibold);
}

.toast-header strong { letter-spacing: var(--tracking-tight); }

.toast-body {
    padding: var(--space-3);
    color: var(--text-body);
    line-height: 1.45;
    /* Long unbreakable strings (filesystem paths, URLs) must wrap rather
       than push the close button offscreen on narrow viewports. */
    overflow-wrap: anywhere;
    word-break: break-word;
}

/* ── Phones: respect the iOS notch + cap width so toasts always fit. ─────
   The `Toasts` component in `Layout/MainLayout.razor` uses TopCenter
   placement which puts `.toast-container` at `top: 0; left: 50%`. Without
   safe-area padding the toast paints over the status bar on iPhone; without
   the max-width cap a toast at min-width 300px can equal the viewport on
   narrow screens (e.g. iPhone SE landscape rotation). */
@media (max-width: 640.98px) {
    .toast-container {
        padding-top: calc(var(--space-3) + env(safe-area-inset-top, 0px));
        padding-left: calc(var(--space-3) + env(safe-area-inset-left, 0px));
        padding-right: calc(var(--space-3) + env(safe-area-inset-right, 0px));
        padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
        max-width: 100vw;
    }
    .toast {
        min-width: 0;
        max-width: calc(100vw - var(--space-4) * 2);
        width: 100%;
    }
}

/* Variant left-accent bar keyed off the Bootstrap text-* / bg-* classes that
   BlazorBootstrap stamps on based on ToastType. Matches the output-row colours
   in the editor dock so the visual language is consistent. */
.toast.text-bg-success,
.toast.bg-success { border-left: 3px solid var(--success); }
.toast.text-bg-danger,
.toast.bg-danger  { border-left: 3px solid var(--danger); }
.toast.text-bg-warning,
.toast.bg-warning { border-left: 3px solid var(--warning); }
.toast.text-bg-info,
.toast.bg-info,
.toast.text-bg-primary,
.toast.bg-primary { border-left: 3px solid var(--kmila-accent); }

/* Bootstrap sets text-bg-* with a coloured background; strip it so we keep
   our surface-1 tone and rely on the left accent + header icon for variant. */
.toast.text-bg-success,
.toast.text-bg-danger,
.toast.text-bg-warning,
.toast.text-bg-info,
.toast.text-bg-primary,
.toast.text-bg-secondary {
    background-color: var(--surface-1) !important;
    color: var(--text-body) !important;
}

.toast .btn-close {
    filter: invert(0.5);
    opacity: 0.7;
    transition: opacity var(--duration-fast) var(--ease-standard);
}
.toast .btn-close:hover { opacity: 1; }

[data-bs-theme="dark"] .toast .btn-close { filter: invert(0.85); }

/* ── Modal X close — theme-aware ────────────────────────────────────────────
   BB's `.btn-close` is a CSS-painted X via a hardcoded data-uri background.
   Bootstrap 5.3 ships a dark-mode swap as part of its base styles, but the
   colour is muted; bump opacity + reuse the toast invert trick on dark theme
   so the X is legible on `--surface-2`. */
.modal .btn-close {
    opacity: 0.7;
    transition: opacity var(--duration-fast) var(--ease-standard);
}
.modal .btn-close:hover { opacity: 1; }
[data-bs-theme="dark"] .modal .btn-close { filter: invert(0.92); }

/* RTL: accent bar flips to the right edge. */
[dir="rtl"] .toast.text-bg-success,
[dir="rtl"] .toast.bg-success,
[dir="rtl"] .toast.text-bg-danger,
[dir="rtl"] .toast.bg-danger,
[dir="rtl"] .toast.text-bg-warning,
[dir="rtl"] .toast.bg-warning,
[dir="rtl"] .toast.text-bg-info,
[dir="rtl"] .toast.bg-info,
[dir="rtl"] .toast.text-bg-primary,
[dir="rtl"] .toast.bg-primary {
    border-left: none;
    border-right: 3px solid;
    /* Same colours, just mirrored — re-apply per variant so the current hue
       still wins: */
}
[dir="rtl"] .toast.text-bg-success,
[dir="rtl"] .toast.bg-success  { border-right-color: var(--success); }
[dir="rtl"] .toast.text-bg-danger,
[dir="rtl"] .toast.bg-danger   { border-right-color: var(--danger); }
[dir="rtl"] .toast.text-bg-warning,
[dir="rtl"] .toast.bg-warning  { border-right-color: var(--warning); }
[dir="rtl"] .toast.text-bg-info,
[dir="rtl"] .toast.bg-info,
[dir="rtl"] .toast.text-bg-primary,
[dir="rtl"] .toast.bg-primary  { border-right-color: var(--kmila-accent); }


/* Learn hero: search field and clear button */
.learn-hero__search {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-3);
    max-width: 420px;
    background-color: var(--surface-0);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    padding: 0 var(--space-2) 0 var(--space-3);
    color: var(--text-muted);
}

.learn-hero__search:focus-within {
    border-color: var(--kmila-accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--kmila-accent) 15%, transparent);
}

.learn-hero__search input[type="search"] {
    flex: 1 1 auto;
    border: none;
    background: transparent;
    color: var(--text-strong);
    font-size: var(--fs-sm);
    padding: var(--space-2) 0;
}
.learn-hero__search input[type="search"]:focus { outline: none; box-shadow: none; }

/* Hide the native clear cross — we render our own when needed. */
.learn-hero__search input[type="search"]::-webkit-search-cancel-button { display: none; }
.learn-hero__search input[type="search"]::-webkit-search-decoration { display: none; }

.learn-hero__search-clear {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    background: transparent;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
    border-radius: var(--radius-xs);
}
.learn-hero__search-clear:hover { background-color: var(--surface-2); color: var(--text-strong); }


/* Sidebar nav item: progress badge (used for Learn "3/27") */
.kmila-nav-item__badge {
    margin-left: auto;
    padding: 1px var(--space-2);
    border-radius: 999px;
    background-color: color-mix(in srgb, var(--kmila-accent) 20%, transparent);
    color: #ffffff;
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-bold);
    letter-spacing: var(--tracking-tight);
    line-height: 1.4;
    white-space: nowrap;
}

[dir="rtl"] .kmila-nav-item__badge { margin-left: 0; margin-right: auto; }

.kmila-sidebar--collapsed .kmila-nav-item__badge { display: none; }


/* Home hero: progress chip on the Learn CTA */
.home-hero__progress-chip {
    display: inline-flex;
    align-items: center;
    margin-left: var(--space-2);
    padding: 1px var(--space-2);
    border-radius: 999px;
    background-color: color-mix(in srgb, var(--kmila-accent) 15%, transparent);
    color: var(--kmila-accent);
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-bold);
    line-height: 1.4;
}
[dir="rtl"] .home-hero__progress-chip { margin-left: 0; margin-right: var(--space-2); }


/* ----------------------------------------------------------------------------
   WaveformViewer — tokenized chrome matching the Editor dock
   ---------------------------------------------------------------------------- */

.waveform-viewer {
    display: flex;
    flex-direction: column;
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    overflow: hidden;
    box-shadow: var(--shadow-1);
}

/* Banner shown on the Waveforms tab when the user opened a saved run while a
   live simulation for the same project is still in flight. The "return to
   live run" button drops the suspension and rehydrates from the engine. */
.waveform-saved-banner {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    margin-bottom: var(--space-2);
    background-color: var(--surface-2);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-md);
    font-size: var(--fs-sm);
    color: var(--text-2);
}

.waveform-saved-banner > span {
    flex: 1 1 auto;
    min-width: 0;
}

.waveform-saved-banner > .kmila-btn {
    flex: 0 0 auto;
}

.waveform-viewer__toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-2);
    border-bottom: 1px solid var(--border-subtle);
    min-height: 44px;
    /* In Spanish + dark themes the title gets long ("Visualizador de formas
       de onda · N señales") and the export/zoom buttons used to push off the
       card edge with no wrap fallback. Let the toolbar wrap onto a second
       row when needed; the title cluster keeps its `min-width:0` so it
       ellipsizes first. */
    flex-wrap: wrap;
    /* Pin to the top of the dock panel's scroll viewport so the zoom /
       compare / export controls stay reachable when the signal list is
       long enough to require scrolling. The panel (.kmila-dock__panel)
       is the actual vertical-scroll container; the toolbar's surface
       colour is already opaque, so signal rows scrolling underneath it
       remain hidden. z-index keeps it above the SVG strokes that would
       otherwise paint over its bottom border on some browsers. */
    position: sticky;
    top: 0;
    z-index: 2;
}

.waveform-viewer__title {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--text-strong);
    font-weight: var(--fw-semibold);
    /* Allow the title to ellipsize when the toolbar gets cramped in long
       locales (Spanish "Visualizador de formas de onda" + signal count). */
    min-width: 0;
    flex: 1 1 auto;
}
.waveform-viewer__title > :last-child {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.waveform-viewer__chip {
    padding: 1px var(--space-2);
    border-radius: 999px;
    background-color: color-mix(in srgb, var(--kmila-accent) 15%, transparent);
    color: var(--kmila-accent);
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-bold);
}

.waveform-viewer__actions {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
}

.waveform-viewer__body {
    padding: var(--space-2);
    overflow-x: auto;
}

.waveform-signal-chip {
    display: inline-flex;
    align-items: center;
    padding: 1px var(--space-2);
    border-radius: 999px;
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-bold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
}
.waveform-signal-chip--in       { background: color-mix(in srgb, var(--kmila-accent) 15%, transparent); color: var(--kmila-accent); }
.waveform-signal-chip--out      { background: rgba(245, 158, 11, 0.15); color: var(--warning); }
.waveform-signal-chip--inout    { background: rgba(168, 85, 247, 0.15); color: #c084fc; }
.waveform-signal-chip--internal { background: var(--surface-0);         color: var(--text-muted); }

.waveform-signal-name {
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
}

.waveform-signal-width {
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
    color: var(--text-muted);
}


/* ----------------------------------------------------------------------------
   Accessibility
   ---------------------------------------------------------------------------- */

/* Skip-to-content link — visually hidden until focused. Parks above the
   sidebar so keyboard users can bypass navigation on every page load. */
.kmila-skip-link {
    position: absolute;
    top: var(--space-2);
    left: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background-color: var(--kmila-accent);
    color: #ffffff;
    font-weight: var(--fw-semibold);
    text-decoration: none;
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-2);
    transform: translateY(-150%);
    opacity: 0;
    transition: transform var(--duration-fast) var(--ease-standard),
                opacity var(--duration-fast) var(--ease-standard);
    z-index: calc(var(--z-toast) + 10);
}

.kmila-skip-link:focus-visible,
.kmila-skip-link:focus {
    transform: translateY(0);
    opacity: 1;
    outline: 2px solid #ffffff;
    outline-offset: 2px;
}

[dir="rtl"] .kmila-skip-link { left: auto; right: var(--space-2); }

/* Baseline focus ring for every keyboard-reachable control that doesn't
   already define one. Uses the brand accent at 2px — visible against every
   surface token without being obnoxious. */
:is(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
    outline: 2px solid var(--kmila-accent);
    outline-offset: 2px;
    border-radius: var(--radius-xs);
}

/* Respect the user's motion preference. Kills all non-essential transitions
   + animations when they've asked the OS to reduce motion. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
        scroll-behavior: auto !important;
    }
}


/* ----------------------------------------------------------------------------
   WaveformViewer v2 — inline-SVG grid
   ----------------------------------------------------------------------------
   Critical containment rules. We push the inline SVG as wide as 24 000 px at
   max zoom — without proper clipping, a flex ancestor would expand to fit
   and shove the whole editor (Run bar, FPGA picker, TopBar) off screen.
   The recipe:
     .waveform-viewer        → min-width: 0 + overflow: hidden  (never grow parents)
     .waveform-viewer__body  → the ONLY scroll container (horizontal)
     .waveform-grid          → width: max-content, contained by the body above
*/

/* `contain: inline-size` is the CSS-spec way to tell the layout engine
   "this element's width is independent of its content". Every zoom attempt
   before this was chasing max-content leaks at different ancestors — this
   cuts the whole chain at the viewer in one line. Width is 100 % of the
   dock panel; a 24 000 px inner grid scrolls within the body, period. */
.waveform-viewer {
    width: 100%;
    min-width: 0;
    max-width: 100%;
    /* Flex column so the body row can `flex: 1; min-height: 0` and absorb the
       remaining height inside its parent (.kmila-dock__panel--waveforms,
       which is itself a flex column). Without this the body would shrink to
       its content's max-height and a SECOND outer scrollbar would appear on
       the panel — that's the "third scroll" UX issue. */
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
    contain: inline-size;
}
.waveform-viewer__body {
    width: 100%;
    min-width: 0;
    /* Vertical scroll lives on the body — a sim with many ports (e.g. CPU
       register file) used to be clipped at 70vh with no way to reach the
       rows below. The body now fills whatever vertical space the panel
       offers; the grid grows to its content; the body shows a scrollbar
       when its content exceeds the available rect. Mouse wheel without
       ctrl/cmd scrolls natively. */
    overflow-x: auto;
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0;
    contain: inline-size;
    /* Containing block for the JS hover cursor's absolutely-positioned
       line + chip (appended into the body by kmilaAttachWaveformHover).
       Without this the chip's top:scrollTop+4 anchors to the nearest
       positioned ancestor — usually .kmila-dock — and visually drifts up
       to the toolbar/tabs region. contain:inline-size + overflow:auto
       don't establish a positioning context by themselves; only an
       explicit `position` does. */
    position: relative;
    /* Native touch scroll. The shared gesture-cursor rule below assumes the
       body is a custom pan-zoom surface (true for schematic + flow, false
       for the waveform after we detached pan-zoom from it). Restore the
       defaults explicitly so touch users on iOS/Android get a normal scroll
       experience instead of the page being frozen. */
    touch-action: auto;
    cursor: auto;
    user-select: auto;
    -webkit-user-select: auto;
}

.waveform-grid {
    display: flex;
    flex-direction: column;
    background-color: var(--surface-0);
    border-radius: var(--radius-md);
    width: max-content;
}

.waveform-row {
    display: flex;
    align-items: stretch;
    min-height: 44px;
    border-bottom: 1px solid var(--border-subtle);
}
.waveform-row:last-child { border-bottom: none; }

.waveform-row--ruler { min-height: 26px; }

.waveform-row__label {
    flex: 0 0 160px;
    min-width: 160px;
    padding: var(--space-1) var(--space-2);
    background-color: var(--surface-1);
    border-right: 1px solid var(--border-subtle);
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    font-size: var(--fs-xs);
    flex-wrap: wrap;
    /* Sticks to the left edge of the scrollable body — signal name stays
       readable even after scrolling 24 k pixels of trace. The right-edge
       shadow makes the pinning visible: at zoom-out the column sits flush
       against the trace and the shadow is invisible; once the user scrolls
       horizontally the trace slides under the column and the shadow gives
       a clear depth cue that "this column stays put". */
    position: sticky;
    left: 0;
    z-index: 2;
    /* Soft cue, not a band. Sticky label shadow uses the shared
       --shadow-sticky token so light + dark stay calibrated; previous
       values bloomed into a hard halo on dark mode. */
    box-shadow: var(--shadow-sticky);
}

/* Top-row label is the first row of the grid; we also make the ruler row's
   label cell stick to the BOTTOM of the body so the time axis stays visible
   as the user scrolls vertically through many signals — same idea as the
   left column, just on the other axis. */
.waveform-row--ruler {
    position: sticky;
    bottom: 0;
    z-index: 3;
    background-color: var(--surface-0);
}
.waveform-row--ruler .waveform-row__label {
    /* Ruler label cell stays at the bottom-left intersection — needs the
       higher z-index so it sits above the regular sticky column above it. */
    z-index: 4;
}

.waveform-row__track {
    position: relative;
    background-color: var(--surface-1);
    flex-shrink: 0;
    /* width set inline — equals the FULL content width (pxPerNs × totalNs)
       so the body has something wide to scroll. The actual SVG inside
       lives in .waveform-row__track-view (below) and renders only the
       visible time window, regardless of the track's outer width. */
}

/* Virtualised render surface. Sits inside .waveform-row__track at
   position: sticky; left: 160px so it stays glued to the visible
   viewport (right after the sticky label column) as the user scrolls
   horizontally. The SVG inside is sized to the visible viewport width
   only (set inline) — never to the full content width — so neither the
   path string nor the rendered SVG element scale with total sim length.
   Width is updated by Blazor whenever JS reports a resize/scroll. */
.waveform-row__track-view {
    position: sticky;
    left: 160px;
    height: 100%;
    /* width set inline — equals body.clientWidth - 160 (approx).
       The SVG inside renders at this exact size; transitions outside
       the visible time window are skipped by BuildTraceSvg. */
}

.waveform-svg          { display: block; height: 40px; width: 100%; }
.waveform-svg--ruler   { height: 22px; }

/* Click-to-hide affordance on signal labels: cursor changes, the small
   eye-slash icon to the right fades in on hover so the user has visible
   confirmation that the row is interactive. Keeps the existing label flex
   layout intact — the icon just becomes the trailing flex item. */
.waveform-row__label--clickable { cursor: pointer; }
.waveform-row__label-eye {
    margin-left: auto;
    opacity: 0;
    transition: opacity 120ms ease-in-out;
    font-size: var(--fs-xs);
    color: var(--text-muted);
}
.waveform-row__label--clickable:hover .waveform-row__label-eye { opacity: 0.7; }
.waveform-row__label--clickable:hover { background-color: var(--surface-2); }

/* Value column + per-signal waveform/value toggle (backlog: numeric signals as
   values). The 160px label column now packs: [main: chip+name+width] on the
   left (grows, truncates), then the value-at-cursor readout and the toggle
   pinned right. nowrap keeps every row exactly one line tall so the track SVGs
   and the ruler stay aligned. */
.waveform-row__label { flex-wrap: nowrap; overflow: hidden; }
.waveform-row__label-main {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-width: 0;
    flex: 1 1 auto;
    overflow: hidden;
}
.waveform-row__label-main .waveform-signal-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.waveform-signal-value {
    flex: 0 0 auto;
    font-family: ui-monospace, Menlo, monospace;
    font-size: var(--fs-xs);
    font-weight: 600;
    max-width: 66px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.waveform-row__mode-toggle {
    flex: 0 0 auto;
    width: 18px;
    height: 18px;
    padding: 0;
    line-height: 1;
    font-size: 12px;
    cursor: pointer;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    color: var(--text-muted);
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.waveform-row__mode-toggle:hover { color: var(--text-strong); background: var(--surface-3); }

/* Value-only track: a large monospace readout replaces the SVG for the row. */
.waveform-row__value-track {
    display: flex;
    align-items: center;
    height: 40px;
    padding-left: var(--space-2);
}
.waveform-value-readout {
    font-family: ui-monospace, Menlo, monospace;
    font-size: var(--fs-sm);
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* Direction pill ↔ name separation. Previously the uppercase direction chip
   sat 4px from the mono name, so a name containing "in"/"out"/"internal" read
   as part of the type. A slim divider + breathing room to the LEFT of the name
   makes the pill unambiguously the type. Applies to both single and compare
   mode (both now wrap chip+name+width in .waveform-row__label-main). */
.waveform-row__label-main .waveform-signal-chip { margin-right: var(--space-1); }
.waveform-row__label-main .waveform-signal-name {
    padding-left: var(--space-2);
    border-left: 1px solid var(--border-default);
}

/* Vector expand/collapse chevron (bus → per-bit waves). Sits as the first item
   in the label; a same-width spacer keeps non-vector rows' chips aligned. */
.waveform-row__expand-toggle {
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    padding: 0;
    line-height: 1;
    font-size: 10px;
    cursor: pointer;
    border: none;
    background: transparent;
    color: var(--text-muted);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-sm);
}
.waveform-row__expand-toggle:hover { color: var(--text-strong); background: var(--surface-2); }
.waveform-row__expand-spacer { flex: 0 0 16px; width: 16px; }

/* Expanded bit rows: one std_logic wave per bit, indented + muted under their
   parent bus. A left inset rule signals the nesting; the label keeps the same
   160px sticky width so every track SVG stays column-aligned with the parent. */
.waveform-row--bit .waveform-row__label {
    padding-left: var(--space-4);
    box-shadow: inset 2px 0 0 var(--border-default);
    background-color: var(--surface-0);
}
.waveform-row--bit .waveform-row__bit-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    color: var(--text-muted);
}

/* Toolbar-level chip-bar listing currently-hidden signals. Each chip restores
   a single signal on click; the trailing "Show all" button bulk-restores. The
   bar only renders when at least one signal is hidden, so it doesn't add
   visual weight to the common case. */
.waveform-viewer__hidden {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-2);
    border-bottom: 1px solid var(--border-subtle);
    font-size: var(--fs-xs);
}
.waveform-viewer__hidden-label {
    color: var(--text-muted);
    font-weight: 600;
}
.waveform-hidden-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 2px 8px;
    background-color: var(--surface-0);
    border: 1px solid var(--border-default);
    border-radius: 999px;
    font-family: ui-monospace, Menlo, monospace;
    font-size: var(--fs-xs);
    color: var(--text-strong);
    cursor: pointer;
    transition: background-color 120ms ease, border-color 120ms ease;
}
.waveform-hidden-chip:hover {
    background-color: var(--surface-1);
    border-color: var(--accent);
    color: var(--accent);
}

/* Hover cursor: vertical guide line + time chip injected by
   kmilaAttachWaveformHover. Both elements sit inside .waveform-viewer__body
   in content coordinates; the JS recomputes positions on mousemove/scroll
   so the line tracks the cursor visually while the chip stays anchored to
   the top of the visible body. pointer-events: none keeps them from
   intercepting clicks on the underlying traces. */
.waveform-hover__line {
    position: absolute;
    top: 0;
    width: 1px;
    background-color: var(--accent);
    opacity: 0.7;
    pointer-events: none;
    z-index: 5;
}
.waveform-hover__chip {
    position: absolute;
    padding: 2px 6px;
    background-color: var(--surface-3, var(--surface-2));
    color: var(--text-strong);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-sm);
    font-family: ui-monospace, Menlo, monospace;
    font-size: var(--fs-2xs);
    line-height: 1.3;
    pointer-events: none;
    white-space: nowrap;
    z-index: 6;
    box-shadow: var(--shadow-1);
}


/* Sidebar project row: "simulation is running" pulse. Drives from
   SimulationParameters.RunningProjectId — stays on even when the user
   navigates away from /editor/{id}, which is the whole point of
   background sims. */
.kmila-nav-item__pulse {
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-left: auto;
    border-radius: 50%;
    background-color: var(--success);
    box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.6);
    animation: kmilaRunPulse 1.6s ease-out infinite;
    flex-shrink: 0;
}
[dir="rtl"] .kmila-nav-item__pulse { margin-left: 0; margin-right: auto; }

@keyframes kmilaRunPulse {
    0%   { box-shadow: 0 0 0 0   rgba(16, 185, 129, 0.6); }
    70%  { box-shadow: 0 0 0 8px rgba(16, 185, 129, 0);   }
    100% { box-shadow: 0 0 0 0   rgba(16, 185, 129, 0);   }
}

.kmila-sidebar--collapsed .kmila-nav-item__pulse {
    position: absolute;
    top: 8px; right: 8px;
    margin: 0;
}


/* ----------------------------------------------------------------------------
   Sidebar background-sim controls — pause / resume / stop visible while
   Sim.RunningProjectId == this project, so a user who navigated away from
   /editor/{id} can still steer their long sim without coming back.
   ---------------------------------------------------------------------------- */

/* Inline "47%" chip in the project row label. Sits between the name and the
   pulse dot. Kept narrow + muted so it doesn't dominate the row. */
.kmila-nav-item__progress-text {
    margin-left: auto;
    padding: 0 6px;
    font-size: var(--fs-2xs);
    font-variant-numeric: tabular-nums;
    color: rgba(255, 255, 255, 0.7);
    flex-shrink: 0;
}
[dir="rtl"] .kmila-nav-item__progress-text { margin-left: 0; margin-right: auto; }

/* When the % chip is present, the pulse dot should sit immediately after it,
   not push to the row edge. Removing margin-left:auto on pulse only when a
   sibling % is present keeps the layout tight. */
.kmila-nav-item__progress-text + .kmila-nav-item__pulse {
    margin-left: 0;
}

/* Pause / Resume / Stop always visible while running — unlike the trash
   button, which hides until hover. The controls need to be reachable on
   touch devices and on first sight, since this is the whole point of the
   background-sim surface. */
.kmila-nav-item--running .kmila-nav-item__actions {
    opacity: 1;
    pointer-events: auto;
}

/* Make room on the right of the project name for the always-on controls. */
.kmila-nav-item--running .kmila-nav-item__main {
    padding-right: 64px;
}
[dir="rtl"] .kmila-nav-item--running .kmila-nav-item__main {
    padding-right: var(--space-4);
    padding-left: 64px;
}

/* The default action:hover turns red (matches the trash semantics).
   For Pause / Resume that's misleading — use the accent color instead.
   Only the Stop button (--danger) keeps the red hover. */
.kmila-nav-item__action:not(.kmila-nav-item__action--danger):hover {
    background-color: rgba(255, 255, 255, 0.18);
    color: #ffffff;
}

.kmila-nav-item__action--danger:hover {
    background-color: rgba(239, 68, 68, 0.35);
    color: #ffffff;
}

/* Slim progress strip pinned to the bottom of the row. 2px so it reads as
   an accent stripe rather than a chunky bar. */
.kmila-nav-item__progress {
    position: absolute;
    left: var(--space-4);
    right: var(--space-4);
    bottom: 2px;
    height: 2px;
    background-color: rgba(255, 255, 255, 0.08);
    border-radius: 1px;
    overflow: hidden;
    pointer-events: none;
}

.kmila-nav-item__progress-fill {
    display: block;
    height: 100%;
    background-color: var(--kmila-accent);
    transition: width var(--duration-base) var(--ease-standard);
}

.kmila-nav-item--running .kmila-nav-item__progress-fill {
    background: linear-gradient(
        90deg,
        var(--kmila-accent) 0%,
        color-mix(in srgb, var(--kmila-accent) 60%, transparent) 50%,
        var(--kmila-accent) 100%
    );
    background-size: 200% 100%;
    animation: kmilaShimmerProgress 1.8s linear infinite;
}

/* Collapsed (rail) mode: progress strip + % text don't fit. Drop them; the
   pulse dot at the corner still signals "running." Stop button slides to the
   left of the pulse so it's reachable via the rail's hover popover layer. */
.kmila-sidebar--collapsed .kmila-nav-item__progress,
.kmila-sidebar--collapsed .kmila-nav-item__progress-text {
    display: none;
}


/* ----------------------------------------------------------------------------
   Project card running state — same affordances as the sidebar, with more
   horizontal room so we can spell out "Running 47%" in a pill.
   ---------------------------------------------------------------------------- */

.project-card--running {
    border-color: color-mix(in srgb, var(--kmila-accent) 35%, var(--border-default));
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--kmila-accent) 25%, transparent),
                var(--shadow-2);
}

/* Always show actions when running — same reasoning as the sidebar:
   controls must be reachable at first glance. */
.project-card--running .project-card__actions {
    opacity: 1;
    pointer-events: auto;
}

/* "Running · 47%" / "Paused · 47%" pill next to the project name. */
.project-card__running-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-left: var(--space-2);
    padding: 2px var(--space-2);
    border-radius: 999px;
    background-color: color-mix(in srgb, var(--kmila-accent) 15%, transparent);
    color: var(--kmila-accent);
    font-size: var(--fs-2xs);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    vertical-align: middle;
}
[dir="rtl"] .project-card__running-badge {
    margin-left: 0;
    margin-right: var(--space-2);
}

.project-card__running-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background-color: var(--kmila-accent);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--kmila-accent) 60%, transparent);
    animation: kmilaRunPulse 1.6s ease-out infinite;
    flex-shrink: 0;
}

/* Visual-mode chip beside the "Running/Paused" pill — distinguishes a
   visual sim (paused when its tab is hidden, stopped on project change)
   from a signal-only background sim (persists across navigation). Uses
   a muted surface tone so the primary running pill stays dominant. */
.project-card__mode-chip,
.kmila-nav-item__mode-chip {
    display: inline-flex;
    align-items: center;
    margin-left: var(--space-2);
    padding: 1px var(--space-2);
    border-radius: 999px;
    background-color: var(--surface-3);
    color: var(--kmila-text-muted, var(--kmila-muted, #667));
    font-size: var(--fs-2xs);
    font-weight: 600;
    letter-spacing: 0.02em;
    vertical-align: middle;
}
[dir="rtl"] .project-card__mode-chip,
[dir="rtl"] .kmila-nav-item__mode-chip {
    margin-left: 0;
    margin-right: var(--space-2);
}

/* The progress strip sits below the description, hugging the body width. */
.project-card__progress {
    margin-top: var(--space-2);
    height: 3px;
    background-color: var(--surface-3);
    border-radius: 2px;
    overflow: hidden;
}

.project-card__progress-fill {
    display: block;
    height: 100%;
    background: linear-gradient(
        90deg,
        var(--kmila-accent) 0%,
        color-mix(in srgb, var(--kmila-accent) 60%, transparent) 50%,
        var(--kmila-accent) 100%
    );
    background-size: 200% 100%;
    animation: kmilaShimmerProgress 1.8s linear infinite;
    transition: width var(--duration-base) var(--ease-standard);
}

/* Resume button — the "primary" action while paused. Tinted accent so the
   user can tell it apart from the stop button at a glance. */
.project-card__action--accent {
    background-color: color-mix(in srgb, var(--kmila-accent) 18%, var(--surface-2));
    border-color: color-mix(in srgb, var(--kmila-accent) 40%, var(--border-subtle));
    color: var(--kmila-accent);
}

.project-card__action--accent:hover {
    background-color: color-mix(in srgb, var(--kmila-accent) 28%, var(--surface-2));
    color: var(--kmila-accent);
    border-color: var(--kmila-accent);
}


/* ----------------------------------------------------------------------------
   Waveform compare mode
   ---------------------------------------------------------------------------- */

.waveform-viewer__legend {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-2);
    border-bottom: 1px solid var(--border-subtle);
}

.waveform-legend-pill {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 2px var(--space-2);
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: 999px;
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-strong);
}

.waveform-legend-swatch {
    width: 10px;
    height: 10px;
    border-radius: 50%;
}

/* Runs dock tab — snapshot list */
.runs-panel {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.runs-panel__compare-bar {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background-color: color-mix(in srgb, var(--kmila-accent) 8%, transparent);
    border: 1px solid color-mix(in srgb, var(--kmila-accent) 30%, transparent);
    border-radius: var(--radius-md);
    color: var(--text-strong);
    font-size: var(--fs-sm);
}

.runs-panel__compare-bar button:last-child { margin-left: auto; }

.runs-panel__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--space-2);
}

.runs-panel__item {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    transition: border-color var(--duration-fast) var(--ease-standard);
}
.runs-panel__item:hover { border-color: var(--kmila-accent); }
.runs-panel__item.is-selected {
    border-color: var(--kmila-accent);
    background-color: color-mix(in srgb, var(--kmila-accent) 6%, transparent);
}
.runs-panel__item.is-pinned { border-left: 3px solid var(--warning); }

.runs-panel__select {
    margin: 0;
    display: inline-flex;
    align-items: center;
    cursor: pointer;
}

.runs-panel__main {
    background: transparent;
    border: none;
    text-align: left;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
    cursor: pointer;
    color: inherit;
    padding: 0;
}

.runs-panel__head { display: flex; align-items: center; gap: var(--space-2); }
.runs-panel__label { font-weight: var(--fw-semibold); color: var(--text-strong); }
.runs-panel__pin-chip { color: var(--warning); font-size: 0.85em; }

.runs-panel__meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    font-size: var(--fs-xs);
    color: var(--text-muted);
}

.runs-panel__chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    font-family: var(--font-mono);
}
.runs-panel__chip--muted { color: var(--text-muted); opacity: 0.8; }

.runs-panel__actions {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    opacity: 0;
    transition: opacity var(--duration-fast) var(--ease-standard);
}
.runs-panel__item:hover .runs-panel__actions,
.runs-panel__item:focus-within .runs-panel__actions { opacity: 1; }


/* Run-bar clock — FPGA-sourced default with visible override chip. */
.run-bar__clock {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 0 var(--space-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background-color: var(--surface-1);
}
.run-bar__clock .run-bar__time-input { max-width: 68px; }
.run-bar__clock-unit {
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
    color: var(--text-muted);
    letter-spacing: var(--tracking-tight);
}
.run-bar__clock-reset {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    margin-left: var(--space-1);
    border: none;
    background: transparent;
    color: var(--text-muted);
    border-radius: var(--radius-xs);
    cursor: pointer;
}
.run-bar__clock-reset:hover { background-color: var(--surface-2); color: var(--text-strong); }

/* Run-mode toggle (Fast ↔ Visual). Mirrors the clock group's pill so it reads
   as part of the config cluster; the Fast state gets an accent tint. */
.run-bar__mode {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background-color: var(--surface-1);
    color: var(--text-muted);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    cursor: pointer;
    white-space: nowrap;
}
.run-bar__mode:hover:not(:disabled) { background-color: var(--surface-2); color: var(--text-strong); }
.run-bar__mode:disabled { opacity: 0.55; cursor: not-allowed; }
.run-bar__mode--fast {
    border-color: color-mix(in srgb, var(--kmila-accent) 45%, transparent);
    background: color-mix(in srgb, var(--kmila-accent) 12%, transparent);
    color: var(--kmila-accent);
}
.run-bar__mode-label { line-height: 1; }

/* Customized clock: warning-tinted border + amber input text so the student
   notices they're off the FPGA's default. Hover tooltip explains the risk. */
.run-bar__clock--custom {
    border-color: rgba(245, 158, 11, 0.5);
    background-color: rgba(245, 158, 11, 0.08);
}
.run-bar__clock--custom .run-bar__time-input { color: var(--warning); font-weight: var(--fw-semibold); }

/* Port moments: "system clock" auto-badge on clk/clock ports. */
.port-moment__syslclk-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 2px var(--space-2);
    border-radius: 999px;
    background-color: color-mix(in srgb, var(--kmila-accent) 12%, transparent);
    color: var(--kmila-accent);
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-bold);
    letter-spacing: var(--tracking-wide);
}


/* FPGA picker: manufacturer filter chips + 2-line option rows. */
.kmila-fpga-picker__filters {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
}
/* Manufacturer filter chips: visuals from .kmila-btn-tab. Local class
   only adds the monospace face since manufacturer codes (Altera, Xilinx,
   Lattice…) read better in mono than in the body sans. */
.kmila-fpga-picker__chip { font-family: var(--font-mono); }

.kmila-fpga-picker__option-main {
    display: inline-flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1px;
    min-width: 0;
}
.kmila-fpga-picker__option-meta {
    font-size: var(--fs-2xs);
    color: var(--text-muted);
    font-family: var(--font-mono);
}


/* ----------------------------------------------------------------------------
   EditorTabStrip — VS Code style tab bar above Monaco
   ----------------------------------------------------------------------------
   Colour-treatment matches FilesCard (same per-extension variants) so a tab
   reads as the same "thing" as its file-list row. Active tab has the dock-
   accent underline; dirty tabs swap the close × for a filled dot until you
   hover, mirroring the VS Code "don't lose work to a stray click" pattern.
*/

.editor-tabs {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    background-color: var(--surface-2);
    border-bottom: 1px solid var(--border-subtle);
    /* Hide the scrollbar by default; reveal on hover so wide tab strips
       don't add a permanent grey bar at the top of the editor. */
    scrollbar-width: thin;
    /* Right-edge fade hints at horizontal overflow when long localised tab
       labels (e.g. JP "ステップリプレイ", AR "إعادة التشغيل خطوة بخطوة") push
       the third tab off-screen on phone widths. Mask works in both themes
       because it operates on alpha, not color. */
    -webkit-mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
    mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
}
[dir="rtl"] .editor-tabs {
    -webkit-mask-image: linear-gradient(to left, black calc(100% - 24px), transparent);
    mask-image: linear-gradient(to left, black calc(100% - 24px), transparent);
}

.editor-tab {
    --et-fg: var(--text-muted);
    --et-bg: transparent;
    --et-bg-hover: var(--surface-1);
    --et-bg-active: var(--surface-0);
    --et-accent: var(--text-muted);

    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    flex-shrink: 0;
    padding: var(--space-2) var(--space-3);
    background: var(--et-bg);
    border: none;
    border-right: 1px solid var(--border-subtle);
    color: var(--et-fg);
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    cursor: pointer;
    transition: background-color var(--duration-fast) var(--ease-standard),
                color var(--duration-fast) var(--ease-standard);
    /* Drag preview shadow stays subtle; we already get a translucent ghost
       from the browser. */
    user-select: none;
}

.editor-tab:hover {
    background-color: var(--et-bg-hover);
    color: var(--text-strong);
}

.editor-tab.is-active {
    background-color: var(--et-bg-active);
    color: var(--text-strong);
}

/* Accent underline on the active tab — uses the same accent the variant
   classes pick. Sits at the bottom edge so it doesn't shift the row. */
.editor-tab.is-active::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 2px;
    background-color: var(--et-accent);
}

/* Drop-target highlight while a drag is over a sibling tab. Subtle inset
   ring keeps it visible without the busy "ghost insertion bar" some other
   editors use. */
.editor-tab.is-drop-target {
    box-shadow: inset 0 0 0 2px var(--kmila-accent);
}

/* Variant tints — matching FilesCard's file-row variants by mapping the
   same accent token to --et-accent. */
.editor-tab--vhdl     { --et-accent: var(--kmila-accent); }
.editor-tab--markdown { --et-accent: #06b6d4; }
.editor-tab--text     { --et-accent: var(--text-muted); }
.editor-tab--json     { --et-accent: var(--warning); }
.editor-tab--xml      { --et-accent: #a855f7; }
.editor-tab--other    { --et-accent: var(--text-muted); }

.editor-tab__icon {
    display: inline-flex;
    align-items: center;
    color: var(--et-accent);
    font-size: var(--fs-base);
    flex-shrink: 0;
}

.editor-tab__name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 220px;
}

/* Dirty dot — replaces the close button until the user hovers. */
.editor-tab__dirty-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background-color: var(--warning);
    flex-shrink: 0;
}

/* The close × shares its slot with the dirty dot. We always render the
   wrapper; visibility flips based on hover + dirty state. */
.editor-tab__close-wrap {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
}

.editor-tab__close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    padding: 0;
    border: none;
    background: transparent;
    color: var(--text-muted);
    border-radius: var(--radius-sm);
    cursor: pointer;
    opacity: 0;
    transition: opacity var(--duration-fast) var(--ease-standard),
                background-color var(--duration-fast) var(--ease-standard);
}

.editor-tab__close:hover {
    background-color: var(--surface-3);
    color: var(--text-strong);
}

/* Reveal × on tab hover. For dirty tabs we keep the dot until the user
   moves over the tab — then the × overlays the dot to give them an
   intentional close target. */
.editor-tab:hover .editor-tab__close,
.editor-tab.is-active .editor-tab__close {
    opacity: 1;
}

.editor-tab.is-dirty:not(:hover) .editor-tab__close {
    opacity: 0;
}

.editor-tab.is-dirty:hover .editor-tab__dirty-dot,
.editor-tab.is-dirty.is-active .editor-tab__dirty-dot {
    display: none;
}


/* Editor dock: custom height via --kmila-dock-height CSS variable set by
   the Razor side. Grabber sits on the top edge and provides a north-south
   cursor; its :hover + :active pull out a subtle accent bar so the affordance
   is obvious. */
.kmila-dock {
    min-height: var(--kmila-dock-height, 320px);
    height: var(--kmila-dock-height, 320px);
}

/* When collapsed, drop the custom height so the dock shrinks to just the tab
   strip (the panel is already hidden via the `hidden` attribute). Without this,
   the resize var keeps the box at its dragged size and the collapse only
   looks like it hid the contents. `!important` is needed because the
   `.kmila-dock` rule above also sets a `height` from the same custom property
   and a single-class override otherwise loses on cascade in some scenarios
   (scoped CSS suffixes, hot-reload re-injection). */
.kmila-dock.kmila-dock--collapsed {
    min-height: 0 !important;
    height: auto !important;
}
.kmila-dock.kmila-dock--collapsed .kmila-dock__panel {
    display: none !important;
}

.kmila-dock__grabber {
    position: relative;
    height: 6px;
    margin-top: -3px;
    cursor: ns-resize;
    background-color: transparent;
    touch-action: none;
    z-index: 3;
}
.kmila-dock__grabber::after {
    content: "";
    position: absolute;
    inset: 2px 0 2px 0;
    background-color: transparent;
    border-radius: 999px;
    transition: background-color var(--duration-fast) var(--ease-standard);
}
.kmila-dock__grabber:hover::after,
.kmila-dock__grabber:active::after {
    background-color: color-mix(in srgb, var(--kmila-accent) 40%, transparent);
}
.kmila-dock--collapsed .kmila-dock__grabber { display: none; }
/* Touch devices need a larger hit zone. Keep the visible "rail" the same
   thickness via ::after, but expand the parent's touch target to ~24px so
   the grabber is actually grabbable with a finger. Also keep the grabber
   rendered when collapsed so swiping up from the rail re-expands the dock
   — previously display:none locked users out of expand-by-drag. */
@media (hover: none) and (pointer: coarse) {
    .kmila-dock__grabber {
        height: 24px;
        margin-top: -12px;
    }
    .kmila-dock__grabber::after {
        inset: 11px 0 11px 0;
    }
    .kmila-dock--collapsed .kmila-dock__grabber {
        display: block;
        height: 18px;
        margin-top: 0;
    }
}


/* ----------------------------------------------------------------------------
   Execution view polish (Run bar · Waveforms chrome · StatusBar)
   ---------------------------------------------------------------------------- */

/* Run-bar row: tighter vertical rhythm, consistent chip heights, aligned
   divider between inputs and actions. */
.run-bar__row {
    align-items: stretch;
    min-height: 44px;
    gap: var(--space-2);
}
.run-bar__fpga,
.run-bar__time,
.run-bar__clock {
    height: 36px;
    align-self: center;
}

/* Disabled-state contrast upgrade — previously looked active-ish in dark mode. */
.run-bar button:disabled,
.run-bar__time-input:disabled,
.run-bar__scale:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

/* StatusBar chips align on the same baseline and share a subtle divider. */
.status-bar {
    padding: 4px var(--space-3);
    gap: var(--space-3);
}
.status-bar__section {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}
.status-bar__section + .status-bar__section {
    border-left: 1px solid var(--border-subtle);
    padding-left: var(--space-3);
}
[dir="rtl"] .status-bar__section + .status-bar__section {
    border-left: none;
    border-right: 1px solid var(--border-subtle);
    padding-left: 0;
    padding-right: var(--space-3);
}

/* Waveform compare legend: wrap cleanly on narrow viewports instead of
   bleeding off the right edge. */
@media (max-width: 720px) {
    .waveform-viewer__legend { padding: var(--space-1) var(--space-2); }
    .waveform-legend-pill { font-size: var(--fs-2xs); }
}

/* Progress line under the Run bar while running — subtle gradient so it's
   felt, not shouted. */
.run-bar__progress { background-color: var(--surface-0); }
.run-bar__progress-fill {
    /* `to right` honours the writing direction in modern browsers, so the
       progress fill animates L→R in LTR and R→L in RTL automatically. The
       previous `90deg` form was direction-blind and shimmered backwards in
       Arabic. */
    background: linear-gradient(to right, var(--kmila-accent) 0%, var(--success) 100%);
}
[dir="rtl"] .run-bar__progress-fill {
    background: linear-gradient(to left, var(--kmila-accent) 0%, var(--success) 100%);
}

/* ==========================================================================
   Reconnect modal — shown when the SignalR circuit drops.
   Element id `components-reconnect-modal` is what Blazor's framework JS
   targets; the framework toggles the state classes below on it.
   Hidden by default — visible only when one of the state classes is set.
   ========================================================================== */
.kmila-reconnect {
    position: fixed;
    inset: 0;
    z-index: calc(var(--z-toast) + 100);   /* above the entire app stack, well clear of toasts */
    display: none;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
    /* opacity transition is handled per-state below so the show animation
       is driven by class changes, not by a JS-controlled style attribute */
}
.kmila-reconnect.components-reconnect-show,
.kmila-reconnect.components-reconnect-failed,
.kmila-reconnect.components-reconnect-rejected { display: flex; }

.kmila-reconnect__backdrop {
    position: absolute;
    inset: 0;
    background-color: rgba(15, 23, 42, 0.55);   /* slate-900 @ 55% */
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    animation: kmila-reconnect-fade 220ms ease-out both;
}

.kmila-reconnect__card {
    position: relative;
    max-width: 420px;
    width: 100%;
    background-color: var(--surface-2);
    color: var(--text-default);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-3);
    overflow: hidden;
    animation: kmila-reconnect-rise 240ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
}

@keyframes kmila-reconnect-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
}
@keyframes kmila-reconnect-rise {
    from { opacity: 0; transform: translateY(8px) scale(0.985); }
    to   { opacity: 1; transform: none; }
}

.kmila-reconnect__head {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-4);
    background-color: var(--surface-1);
    border-bottom: 1px solid var(--border-subtle);
}

.kmila-reconnect__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: var(--radius-md);
    flex-shrink: 0;
    /* Default — actively reconnecting: warning tone */
    background-color: var(--warning-soft);
    color: var(--warning);
}
.kmila-reconnect.components-reconnect-failed .kmila-reconnect__icon,
.kmila-reconnect.components-reconnect-rejected .kmila-reconnect__icon {
    background-color: var(--danger-soft);
    color: var(--danger);
}

.kmila-reconnect__head h2 {
    margin: 0 0 2px;
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    line-height: var(--lh-snug);
}
.kmila-reconnect__subtitle {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--text-muted);
    line-height: var(--lh-snug);
}

.kmila-reconnect__body {
    padding: var(--space-4);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.kmila-reconnect__msg {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--text-default);
    line-height: var(--lh-normal);
    /* Hide all state messages by default; reveal the right one based on which
       state class the framework set on the parent. */
    display: none;
}
.kmila-reconnect.components-reconnect-show     .kmila-reconnect__msg--show     { display: block; }
.kmila-reconnect.components-reconnect-failed   .kmila-reconnect__msg--failed   { display: block; }
.kmila-reconnect.components-reconnect-rejected .kmila-reconnect__msg--rejected { display: block; }

/* Three-dot pulsing spinner — only while actively retrying. */
.kmila-reconnect__spinner {
    display: none;
    align-items: center;
    gap: 6px;
    padding-top: var(--space-1);
}
.kmila-reconnect.components-reconnect-show .kmila-reconnect__spinner { display: flex; }
.kmila-reconnect__spinner span {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background-color: var(--warning);
    opacity: 0.35;
    animation: kmila-reconnect-pulse 1.2s ease-in-out infinite;
}
.kmila-reconnect__spinner span:nth-child(2) { animation-delay: 0.2s; }
.kmila-reconnect__spinner span:nth-child(3) { animation-delay: 0.4s; }
@keyframes kmila-reconnect-pulse {
    0%, 60%, 100% { opacity: 0.35; transform: scale(1); }
    30%           { opacity: 1;    transform: scale(1.2); }
}

.kmila-reconnect__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4) var(--space-4);
    justify-content: flex-end;
    border-top: 1px solid var(--border-subtle);
    background-color: var(--surface-1);
}

.kmila-reconnect__btn {
    appearance: none;
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    padding: 0.45rem 0.9rem;
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    line-height: 1.2;
    cursor: pointer;
    transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease, box-shadow 120ms ease;
}
.kmila-reconnect__btn:focus-visible {
    outline: none;
    box-shadow: var(--shadow-focus);
}
.kmila-reconnect__btn--primary {
    background-color: var(--kmila-accent);
    color: var(--text-on-accent);
    border-color: var(--kmila-accent);
}
.kmila-reconnect__btn--primary:hover  { filter: brightness(1.05); }
.kmila-reconnect__btn--primary:active { filter: brightness(0.95); }
.kmila-reconnect__btn--ghost {
    background-color: transparent;
    color: var(--text-default);
    border-color: var(--border-default);
}
.kmila-reconnect__btn--ghost:hover  { background-color: var(--surface-3); }
.kmila-reconnect__btn--ghost:active { background-color: var(--border-subtle); }

/* While actively retrying, the "Try again" button is redundant — Blazor is
   already attempting reconnects on its own backoff schedule. Hide it so the
   user only sees actionable controls when reconnect has actually given up. */
.kmila-reconnect.components-reconnect-show #components-reconnect-button { display: none; }

/* Mobile: tighter padding, full-width buttons stacked. */
@media (max-width: 480px) {
    .kmila-reconnect__actions { flex-direction: column-reverse; }
    .kmila-reconnect__btn     { width: 100%; }
}

/* RTL: action row flips so primary button stays on the trailing edge. */
[dir="rtl"] .kmila-reconnect__actions { justify-content: flex-start; }


/* ============================================================================
   MOBILE UX OVERHAUL  —  appended block
   ============================================================================
   Single consolidated section that addresses the mobile audit findings:
     • Touch targets ≥44px globally (within tap-target media)
     • Editor tab strip: scroll snap + gradient overflow indicators
     • Pointer-drag tab visuals: ghost + drop target highlight
     • Dock grabber: enlarged hit zone + touch-action
     • Dock as bottom-sheet on mobile (peek/half/full snap classes)
     • Schematic + Waveform: pan/zoom cursor cues + responsive info panel
     • Modals: full-bleed + sticky footer below 640px
     • PortMomentInput: card-stack layout under 480px
     • TopBar: condensed two-row layout option below 640px
     • Settings rows: stacked controls below 640px
     • Markdown / Lesson code blocks: horizontal scroll containment
     • Logs: ellipsis on entries
     • Hover-only effects: gated behind @media (hover: hover) where critical
   ============================================================================ */


/* ── Pointer-drag ghost (rendered into <body>) ───────────────────────────── */
.kmila-tab-ghost {
    pointer-events: none;
    user-select: none;
    -webkit-user-select: none;
    will-change: transform;
}

/* The tab actively being dragged loses its visual presence in the strip
   (the ghost takes over). Keeping width prevents reflow during the drag. */
.editor-tab.is-being-dragged {
    opacity: 0.35;
    filter: grayscale(0.5);
}

/* Drop-target ring lives in JS via DOM class mutation; we declare it here. */
.editor-tab.is-drop-target {
    outline: 2px dashed var(--kmila-accent);
    outline-offset: -2px;
    background-color: var(--kmila-accent-soft);
}


/* ── Editor tabs: scroll snap + overflow gradients ───────────────────────── */
.editor-tabs {
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x proximity;
    /* On touch we want the strip to scroll horizontally, not the page. */
    touch-action: pan-x;
    /* Subtle gradient masks at the strip edges hint at off-screen tabs. */
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%);
            mask-image: linear-gradient(to right, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%);
}
.editor-tab {
    scroll-snap-align: start;
    /* A drag must be a drag, not a scroll start. */
    touch-action: none;
}
.editor-tab__close {
    /* Touch target ≥44px. Keep visual square smaller via the inner icon. */
    min-width: 44px;
    min-height: 32px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
@media (max-width: 768px) {
    .editor-tab__close { min-height: 44px; }
}


/* ── Dock grabber: bigger hit zone + drag affordance ─────────────────────── */
.kmila-dock__grabber {
    /* Visual handle stays slim, but the hit area is doubled via padding. */
    position: relative;
    height: 6px;
    cursor: ns-resize;
    touch-action: none;
    background: transparent;
    flex-shrink: 0;
}
.kmila-dock__grabber::before {
    /* Invisible enlarged tap target — vertical, full-width, +14px each side. */
    content: "";
    position: absolute;
    inset: -14px 0;
    z-index: 1;
}
.kmila-dock__grabber::after {
    /* The visible drag pill — sized for thumbs on mobile. */
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 44px;
    height: 4px;
    border-radius: 999px;
    background: var(--border-strong);
    transition: background-color var(--duration-fast) var(--ease-standard),
                width var(--duration-fast) var(--ease-standard);
}
.kmila-dock__grabber:hover::after,
.kmila-dock__grabber:active::after {
    background: var(--kmila-accent);
    width: 56px;
}


/* ── Dock as bottom-sheet on mobile ──────────────────────────────────────── */
/* The mobile editor stacks four fixed layers from bottom to top:
   StatusBar (--statusbar-h) → Dock (--dock-h) → RunBar (--runbar-h) → TopBar.
   They all read from these custom properties so a snap-class flip on
   .kmila-dock relocates RunBar in lockstep — no per-component math. */
@media (max-width: 768px) {
    body {
        /* StatusBar is hidden on mobile (see .kmila-status-bar rule below),
           so the dock + RunBar can use the full bottom of the viewport. */
        --statusbar-h: 0px;
        --runbar-h: 56px;
        --dock-h: 50dvh;            /* half by default so content is visible
                                       on first paint — drag down for peek */
    }
    body:has(.kmila-dock--snap-peek) { --dock-h: 18dvh; }
    body:has(.kmila-dock--snap-half) { --dock-h: 50dvh; }
    body:has(.kmila-dock--snap-full) { --dock-h: 88dvh; }
    /* R3-1 (editor-mobile audit 2026-07-02, polish 2026-07-02):
       collapsed dock reserves 88 px. First cut was 72 px = tab
       min-height 48 + `.kmila-dock__tabs` top padding 8 + border 1 +
       horizontal scrollbar 5 + 10 slack, but the icon-over-label tab
       column plus the count-pip badge takes closer to 62-72 px of
       content-height, so 72 px left the label 2 px shy of being
       clipped on some devices. 88 px reserves 26 px slack above the
       measured content-height, which is comfortable and still leaves
       Monaco/Blocks/Visual most of the viewport. Kept in sync with
       `.kmila-dock.kmila-dock--collapsed { height: var(--dock-h) }`
       below so both surfaces agree. */
    body:has(.kmila-dock--collapsed) { --dock-h: 88px; }

    .kmila-dock {
        position: fixed;
        left: 0;
        right: 0;
        bottom: calc(var(--statusbar-h) + env(safe-area-inset-bottom, 0px));
        z-index: var(--z-drawer);
        /* R3-1 polish (editor-mobile audit 2026-07-02): the previous
           `box-shadow: 0 -8px 24px rgba(15, 23, 42, 0.18)` cast a 24-px
           blur upward from the dock's top edge, which landed directly on
           the RunBar (RunBar is `position: fixed` and pinned to the dock
           top). The visual result was a dark halo across the RunBar's
           bottom. Removing the shadow here — RunBar has its own border
           and background contrast against Monaco already. */
        box-shadow: none;
        border-top-left-radius: var(--radius-lg);
        border-top-right-radius: var(--radius-lg);
        /* Override the desktop default of `height: var(--kmila-dock-height, 320px)`
           (pages.css:8200-8201) — that 320 px ate RunBar+StatusBar before the
           snap-class controller ran, leaving Run/Build/Clock invisible until
           the user dragged the grabber. */
        height: var(--dock-h) !important;
        min-height: 0 !important;
        max-height: 88dvh;
        transition: height var(--duration-base) var(--ease-standard);
    }

    /* R3-1 (editor-mobile audit 2026-07-02): on mobile, force the
       collapsed dock to the exact same 72 px `--dock-h` reserves.
       Without this override, the desktop `.kmila-dock.kmila-dock--collapsed`
       rule at line ~9456 wins on specificity (2 classes vs 1) and sets
       `height: auto`, leaving the dock box ~62-72 px while `--dock-h`
       is only 56 px → RunBar sits below the dock's top edge and its
       top row gets clipped. Pinning `.kmila-dock.kmila-dock--collapsed`
       to `var(--dock-h)` here keeps the two in lockstep. */
    .kmila-dock.kmila-dock--collapsed {
        height: var(--dock-h) !important;
        min-height: var(--dock-h) !important;
    }

    /* Default + half + full: panel visible (the half-height default lets
       a first-time visitor see content without tapping anything). Peek is
       an explicit opt-in via the grabber-drag snap, and only there do we
       hide the panel so the tab strip alone is shown. */
    .kmila-dock--snap-peek .kmila-dock__panel { display: none; }

    /* Editor workbench needs room for RunBar + dock peek + StatusBar
       so the bottom of Monaco isn't covered. Scoped to the Editor route only —
       previously the comma-selector also matched .kmila-shell__main, which
       added the same padding on every page and left a phantom empty gutter
       below the footer on short pages (audit 2026-06-15 S-05 follow-up). */
    /* R3-3 (editor-mobile audit 2026-07-02): single owner of the dock/
       runbar/statusbar bottom reservation is the shell. Applying the
       same padding to both `.editor-page` and the shell double-counted
       the reservation, pushing the body's intrinsic height past 100dvh
       and opening the outer scrollbar. Keeping the shell as owner lets
       `.content` (flex:1 0 auto) grow to `shell − topbar − padding`, and
       `.editor-page` (flex:1 1 auto; min-height:0) fills that cleanly. */
    .kmila-shell__main:has(.editor-page) {
        padding-bottom: calc(
            var(--dock-h) + var(--runbar-h) + var(--statusbar-h)
            + env(safe-area-inset-bottom, 0px)
        );
    }
}

/* Beefier dock tab icons + tap targets on mobile. The column-flex
   icon-over-label layout lives in the rule at line ~3147; here we just
   ensure the touch target stays >= 44px tall and the horizontal scroll
   scrollbar is visible enough to hint at overflow. */
@media (max-width: 640.98px) {
    .kmila-dock__tab {
        min-height: 48px;
        /* D-2 (editor-mobile audit 2026-06-29): position-anchor for the
           count badge so it lives as a corner pip rather than a third row
           below the label, keeping tab heights uniform across tabs. */
        position: relative;
    }
    .kmila-dock__tabs {
        scroll-snap-type: x proximity;
        scrollbar-width: thin; /* re-enable on mobile to hint at overflow */
    }
    /* D-1 (editor-mobile audit 2026-06-29): bump scrollbar height + tint
       with the brand accent so the overflow cue is visible even before the
       student swipes. 3 px was effectively invisible on Android WebView. */
    .kmila-dock__tabs::-webkit-scrollbar { display: block; height: 5px; }
    .kmila-dock__tabs::-webkit-scrollbar-thumb {
        background: color-mix(in srgb, var(--kmila-accent) 55%, var(--border-strong));
        border-radius: 999px;
    }
    /* D-2: collapse the count from a third row to a top-right corner pip
       on the tab, so the row baseline stays uniform. */
    .kmila-dock__tab .kmila-dock__count {
        position: absolute;
        top: 2px;
        right: 4px;
        min-width: 1.2em;
        height: 1.2em;
        padding: 0 0.25em;
        font-size: calc(10px * var(--user-text-scale, 1));
        line-height: 1.2em;
    }
    [dir="rtl"] .kmila-dock__tab .kmila-dock__count {
        right: auto;
        left: 4px;
    }
    .kmila-dock__collapse {
        min-width: 44px;
        min-height: 44px;
    }
}


/* ── RunBar pinned above the dock peek on mobile ─────────────────────────── */
/* The bar would otherwise float in document flow under the fixed dock and
   become invisible. Pinning to a coordinated bottom offset keeps Run/Build/
   Clocks reachable as the dock snaps between peek/half/full. */
@media (max-width: 768px) {
    .run-bar {
        position: fixed;
        left: 0;
        right: 0;
        bottom: calc(
            var(--dock-h) + var(--statusbar-h) + env(safe-area-inset-bottom, 0px)
        );
        z-index: calc(var(--z-drawer) - 1);
        min-height: var(--runbar-h);
        margin: 0;
        padding-left: max(var(--space-2), env(safe-area-inset-left, 0px));
        padding-right: max(var(--space-2), env(safe-area-inset-right, 0px));
        background-color: var(--surface-1);
        border-top: 1px solid var(--border-subtle);
        /* No upward shadow here: the dock immediately above already casts
           one (0 -8px 24px) and stacking both produced a visible
           double-band on phones. The border-top alone separates the bar
           from the editor body above. */
        box-shadow: none;
        transition: bottom var(--duration-base) var(--ease-standard);
    }
    .run-bar__row {
        /* No wrap on mobile either — the container ladder (TIGHT/CRAMPED)
           folds content into the burger so one row always fits. */
        flex-wrap: nowrap;
        gap: var(--space-2);
        padding-block: var(--space-1);
    }
    /* Full-snap dock takes 88dvh — RunBar would land overlapping TopBar.
       Slide it off-screen instead; the sheet content is what the user wants. */
    body:has(.kmila-dock--snap-full) .run-bar {
        transform: translateY(120%);
        pointer-events: none;
    }
}


/* ── StatusBar: hidden on mobile to give the dock the bottom ────────────────
   The bar shows build status / resource % / sim status / line+column. On
   phones the build state is already visible next to the Compilar button,
   the per-pane header above the editor shows line+column, and resource %
   is one tab away in the Entities panel — so the bar is duplicated info
   eating 32 px the dock could use. Hiding it also fixes a long-standing
   class-name mismatch (the previous rule targeted `.status-bar` but the
   component renders `.kmila-status-bar`, so the bar was never actually
   pinned to the viewport — it floated in document flow under the fixed
   dock and the 32 px below the dock was empty reservation). */
@media (max-width: 768px) {
    .kmila-status-bar { display: none; }
}


/* ── Schematic / Flow: gesture cues + responsive info ────────────────────
   NOTE: .waveform-viewer__body intentionally excluded — the waveform uses
   native scroll (signals are row-structured; 2D pan adds nothing beyond
   what scroll provides, and pan-zoom's cached natural-size bounds went
   stale whenever signals were hidden, letting the user drag content into
   empty space). Schematic and flow are free-form canvases that genuinely
   need the gesture surface. */
.schematic-viewer__body,
.flow-diagram__body {
    /* Pointer events handle the drag; native scroll competes otherwise. */
    touch-action: none;
    cursor: grab;
    /* Suppress text selection while panning. SVG <text> nodes inside gates,
       ports and wire labels were otherwise picked up by the browser's
       drag-to-select when the user held a click before the 2px pan threshold
       fired, leaving a visible "shadow" highlight over content that's clipped
       out of view. */
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
    -webkit-user-drag: none;
}
.schematic-viewer__body:active,
.flow-diagram__body:active { cursor: grabbing; }

/* Hover-only feedback in the schematic stays for desktop, but on touch we
   move it to the active state so taps still get visual confirmation. */
@media (hover: none) {
    .schematic-cell:hover > path:first-child,
    .schematic-wire:hover {
        stroke: inherit;
    }
    .schematic-cell:active > path:first-child {
        stroke: var(--schematic-select);
    }
    .schematic-wire:active {
        stroke: var(--schematic-select);
    }
}

/* Info panel reflows to a bottom sheet style on phones so it doesn't fight
   the gesture surface for the upper-right corner. */
@media (max-width: 640.98px) {
    .schematic-info {
        position: absolute;
        top: auto !important;
        right: 0 !important;
        left: 0 !important;
        bottom: 0 !important;
        width: auto !important;
        max-height: 50dvh;
        border-radius: var(--radius-lg) var(--radius-lg) 0 0;
        box-shadow: 0 -6px 18px rgba(15, 23, 42, 0.25);
    }
    .schematic-basket {
        bottom: 56dvh;
        /* Phone-mode reflow: the basket sits above the bottom-sheet info
           panel and shouldn't crowd the 358px-wide canvas. Drop the
           desktop 240px min-width and let it size to its content. */
        min-width: 0;
        max-width: calc(100% - 2rem);
    }
    /* Toolbar wraps generously and chip groups shrink fonts. */
    .schematic-viewer__toolbar { gap: var(--space-2); }
    .schematic-viewer__modes { margin-left: 0; flex-wrap: wrap; }
    .schematic-mode { padding: 0.4rem 0.6rem; }
    .schematic-mode span { display: none; } /* icons-only on phones */
    /* Phones: stack the toolbar deterministically — title row on top, action
       row on the bottom — instead of relying on flex-wrap. The wrap layout
       was placing the action row's buttons over the first signal row because
       the wrapped flex line's height didn't reliably extend the toolbar's
       intrinsic height (audit 2026-06-16). Stacking + collapsing Imagen/VCD
       to icon-only keeps the toolbar compact and stops the overlap. */
    .waveform-viewer__toolbar {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-2);
        flex-wrap: nowrap;
        /* Reserve vertical space for both stacked rows. Without this floor
           the toolbar's intrinsic height didn't always include the action
           cluster's wrapped buttons and the first signal row could slide
           up underneath them (audit 2026-06-16). */
        min-height: 88px;
    }
    .waveform-viewer__title { flex: 0 0 auto; }
    .waveform-viewer__actions {
        flex: 0 0 auto;
        justify-content: flex-end;
        flex-wrap: wrap;
        gap: 0.25rem;
    }
    .waveform-viewer__actions .kmila-btn--sm > span:not(.spinner-border) {
        display: none;
    }
    .waveform-viewer__actions .kmila-btn--sm {
        padding-inline: var(--space-2);
    }
}


/* ── PortMomentInput: card-stack layout on phones ─────────────────────────── */
@media (max-width: 520px) {
    .port-moment__grid-head { display: none !important; }
    .port-moment__row {
        display: grid !important;
        grid-template-columns: 1fr 1fr auto !important;
        grid-template-areas:
            "time scale remove"
            "value value remove";
        gap: var(--space-2);
        padding: var(--space-3);
        border: 1px solid var(--border-subtle);
        border-radius: var(--radius-md);
        background-color: var(--surface-2);
    }
    .port-moment__time      { grid-area: time; }
    .port-moment__scale     { grid-area: scale; }
    .port-moment__value-wrap { grid-area: value; }
    .port-moment__remove {
        grid-area: remove;
        align-self: start;
        min-width: 44px !important;
        min-height: 44px !important;
    }
    .port-moment__moments { gap: var(--space-3); display: flex; flex-direction: column; }
}


/* ── Tap targets ≥44px globally on phones ─────────────────────────────────── */
@media (max-width: 640.98px) {
    .btn-sm,
    .btn-group-sm > .btn,
    .form-control-sm,
    .form-select-sm {
        min-height: 40px;
        font-size: var(--fs-sm);
    }
    /* Trash / icon-only ghost buttons keep a comfortable tap area. */
    .btn-ghost.btn-sm,
    .btn.btn-sm.kmila-icon-only,
    .kmila-nav-item__action {
        min-width: 44px;
        min-height: 44px;
        padding: 0.4rem 0.6rem;
    }
    /* Custom radios/checkboxes used in export modals get a wrapping label
       with a roomier tap area without changing the visual indicator size. */
    .kmila-export-scope__opt,
    .kmila-export-check {
        padding-block: 0.5rem;
    }
    .kmila-export-scope__opt input[type="radio"],
    .kmila-export-check input[type="checkbox"] {
        width: 22px;
        height: 22px;
    }
}


/* ── TopBar: tight single-row layout on narrow viewports ─────────────────── */
@media (max-width: 640.98px) {
    /* Single-row layout: hamburger + truncated breadcrumb + right-side pills.
       The breadcrumbs already collapse to "last 2 crumbs only" + ellipsis at
       this width (line 612), and the right cluster has flex-shrink: 0 so it
       always stays inside the bar. A previous version forced both halves to
       `flex: 1 1 100%` which wrapped onto two rows — but the <header>'s box
       didn't grow with the wrap, so the right cluster visually leaked out
       below the navy band onto the page body. */
    .kmila-topbar__inner {
        padding: var(--space-2) var(--space-3);
    }
    .kmila-topbar__end {
        gap: var(--space-2);
    }
    /* Account pill collapses to icon-only on the narrowest viewports. */
    .kmila-account-pill__name { display: none; }
    .kmila-fpga-picker__summary { padding: 0.4rem 0.6rem; }
}
@media (max-width: 480px) {
    /* Hide secondary controls on the narrowest phones; they remain reachable
       through the main menu / settings page. */
    .kmila-topbar__end > .kmila-account-pill { display: none; }
}


/* ── Modals: sized cards (sm / md / lg) on every viewport ─────────────────── */
/* Earlier this block forced every modal to 100dvh on phones. Reverting to
   sized "card" modals across all viewports (small / regular / large) so a
   delete-confirm doesn't read like a full-screen takeover and short forms
   feel calmer to dismiss. Bootstrap's native `.modal-sm` / `.modal-lg` /
   `.modal-xl` handle width; we just constrain max-height + give the body
   its own scroll so the footer is always reachable.
   Concrete defects this block still has to defend against:
     • iOS status-bar overlap on top-anchored dialogs → safe-area padding
       on .modal-header.
     • Long unbreakable strings (filesystem paths) overflowing → anywhere-
       break on <p>/<code>, horizontal scroll on <pre>.
     • BlazorBootstrap ConfirmDialog buttons rendering as raw `.btn .btn-
       danger/.btn-secondary` — visually align with the kmila button system. */

/* Phone-only width caps. Bootstrap's defaults make the dialog stretch to
   `100vw - 1rem` below 576px (the .modal-dialog margin: 0.5rem rule with no
   max-width). Re-introduce a max-width per size class so a "small" modal
   reads as a small card even on a 360-px phone. */
@media (max-width: 575.98px) {
    .modal-dialog {
        --kmila-modal-margin: 16px;
        margin: var(--kmila-modal-margin) auto !important;
        width: auto !important;
        max-width: calc(100vw - 2 * var(--kmila-modal-margin)) !important;
    }
    .modal-dialog:not(.modal-sm):not(.modal-lg):not(.modal-xl):not(.modal-fullscreen) {
        /* Regular (default) — keep it compact on phones; medium forms fit easily. */
        max-width: min(420px, calc(100vw - 2 * var(--kmila-modal-margin))) !important;
    }
    .modal-dialog.modal-sm {
        max-width: min(320px, calc(100vw - 2 * var(--kmila-modal-margin))) !important;
    }
    .modal-dialog.modal-lg,
    .modal-dialog.modal-xl {
        max-width: calc(100vw - 2 * var(--kmila-modal-margin)) !important;
    }
}

/* Cross-viewport modal chrome polish — applied to every screen size, not
   just phones, so the card identity is the same on desktop + mobile. */
.modal-content {
    border-radius: var(--radius-lg);
    border: 1px solid var(--border-default);
    box-shadow: var(--shadow-elevated, 0 24px 64px rgba(15, 23, 42, 0.18));
    overflow: hidden;
    display: flex !important;
    flex-direction: column !important;
    /* Cap the visual height at ~85% of the viewport so even tall content
       leaves breathing room above and below the card; the body itself
       scrolls when content overflows. */
    max-height: 85dvh;
}

.modal-header {
    padding-top: calc(var(--space-3) + env(safe-area-inset-top, 0px));
    padding-left: calc(var(--space-3) + env(safe-area-inset-left, 0px));
    padding-right: calc(var(--space-3) + env(safe-area-inset-right, 0px));
    align-items: flex-start;
    background-color: var(--surface-1);
    border-bottom: 1px solid var(--border-subtle);
}

.modal-body {
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    flex: 1 1 auto;
    min-height: 0;
    padding: var(--space-4) !important;
    overflow-wrap: break-word;
}
.modal-body p,
.modal-body code {
    overflow-wrap: anywhere;
    word-break: break-word;
}
.modal-body pre {
    overflow-x: auto;
    white-space: pre;
}

.modal-footer {
    flex: 0 0 auto;
    background-color: var(--surface-2);
    gap: var(--space-2);
    padding-left: calc(var(--space-3) + env(safe-area-inset-left, 0px));
    padding-right: calc(var(--space-3) + env(safe-area-inset-right, 0px));
    padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--border-subtle);
}

/* Footer buttons follow the kmila button family — match radius + focus
   ring + danger/secondary tinting so BlazorBootstrap's ConfirmDialog chrome
   doesn't read as foreign. */
.modal-footer .btn {
    border-radius: var(--radius-sm) !important;
    font-weight: var(--fw-medium);
    font-size: var(--fs-sm);
    transition: background-color var(--duration-fast) var(--ease-standard),
                box-shadow var(--duration-fast) var(--ease-standard),
                transform var(--duration-fast) var(--ease-standard);
}
.modal-footer .btn:focus-visible {
    box-shadow: var(--shadow-focus) !important;
    outline: none;
}
.modal-footer .btn-danger {
    background-color: var(--danger);
    border-color: var(--danger);
}
.modal-footer .btn-secondary {
    background-color: var(--surface-3);
    border-color: var(--border-default);
    color: var(--text-default);
}
.modal-footer .btn-secondary:hover {
    background-color: var(--surface-1);
}

/* Phone-only footer stacking: ConfirmDialog wraps its CTAs in an inner
   `.d-grid` / `<div>:has(> .btn)`. Stack them so the primary CTA is at
   the BOTTOM of the column (closest to the thumb) and Cancel sits above. */
@media (max-width: 575.98px) {
    .modal-footer > .d-grid,
    .modal-footer > div:has(> .btn),
    .modal-footer > div:has(> button) {
        display: flex !important;
        flex-direction: column-reverse !important;
        width: 100%;
        gap: var(--space-2);
        margin: 0 !important;
    }
    .modal-footer .btn,
    .modal-footer button {
        width: 100% !important;
        min-height: 44px;
        margin: 0 !important;
    }
    /* The export modal already stacks; this just reinforces. */
    .kmila-export-form { max-height: none !important; }
}


/* ── Settings rows: stacked controls on phones ────────────────────────────── */
@media (max-width: 640.98px) {
    .settings-row {
        flex-direction: column !important;
        align-items: stretch !important;
        gap: var(--space-2);
    }
    .settings-row__control {
        width: 100% !important;
    }
    .settings-row__control > .form-select,
    .settings-row__control > .form-control {
        width: 100% !important;
        min-height: 44px;
    }
    /* Don't add OUTER padding to the card on phones — the
       .settings-card__head and .settings-card__body each already carry
       their own padding (tightened to var(--space-2) var(--space-3) in
       the @media (max-width: 520px) block above). Stacking an additional
       16px on the card itself produced a visible padding-heavy frame:
       article px-4 (24px) + card outer (16px) + head/body inner (12px)
       = ~52px from screen edge to text on a 320px viewport. */
}


/* ── Markdown / Lesson: horizontal-scroll containment ─────────────────────── */
.lesson-markdown pre,
.markdown-content pre {
    overflow-x: auto;
    max-width: 100%;
    -webkit-overflow-scrolling: touch;
}
/* Tables fill the card width by default — the previous rule applied
   `display: block` so the table could overflow-x scroll, but that turned
   the table into a block-with-anonymous-table whose intrinsic width
   shrinks to content. On narrow lessons (e.g. the AND truth table with
   single-char chips a/b/y) it left the right half of the card empty
   (audit 2026-06-16). Restore `display: table` + `width: 100%` so columns
   distribute across the available width. Long words still won't break
   mid-character (see cell rule below). */
.lesson-markdown table,
.markdown-content table {
    width: 100%;
    max-width: 100%;
    table-layout: auto;
    word-break: normal;
    overflow-wrap: normal;
}
.lesson-markdown table th,
.lesson-markdown table td,
.markdown-content table th,
.markdown-content table td {
    /* Prevent letter-wrapping inside cells — break at word boundaries only. */
    word-break: keep-all;
    overflow-wrap: break-word;
}
.lesson-markdown img,
.markdown-content img {
    max-width: 100%;
    height: auto;
}
@media (max-width: 575.98px) {
    .lesson-markdown { font-size: var(--fs-sm); }
    .lesson-markdown pre,
    .markdown-content pre {
        font-size: var(--fs-xs);
        padding: var(--space-3);
    }
    .lesson-crumb {
        max-width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .lesson-chip-row {
        flex-wrap: wrap;
        gap: var(--space-1);
    }
}


/* ── Logs: ellipsis on long entries ───────────────────────────────────────── */
@media (max-width: 640.98px) {
    .logs-page__list .output-row {
        align-items: flex-start;
    }
    .logs-page__list .output-row > * {
        min-width: 0;
    }
    .logs-row__category {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    /* Logs are diagnostic — truncating with no tap-to-expand left users
       unable to read the full message. Default to wrapped on mobile and
       let the row grow vertically; the category chip still gets ellipsis
       so the row keeps a clean leading column. */
    .logs-page__list .output-row .output-row__msg {
        white-space: pre-wrap;
        word-break: break-word;
    }
    .logs-page__search,
    .logs-page__level {
        flex: 1 1 100%;
        max-width: 100%;
    }
}


/* ── Mobile: schematic fullscreen must escape the dock's stacking context ──
   The dock is `position: fixed; z-index: var(--z-drawer)` on phones, which
   creates a stacking context. A child `.schematic-viewer.is-fullscreen` with
   z-index: calc(--z-modal - 5) is therefore trapped inside the dock's 1020
   layer, painted under the topbar (1021) and notch — toolbar buttons land
   unreachable. When the schematic enters fullscreen we promote the dock
   itself above every chrome layer and let it fill the viewport, and we pad
   the toolbar past the safe-area inset so the exit/mode buttons sit below
   the device notch. */
@media (max-width: 768px) {
    body:has(.schematic-viewer.is-fullscreen) .kmila-dock,
    body:has(.flow-diagram.is-fullscreen) .kmila-dock {
        z-index: calc(var(--z-modal) + 5) !important;
        inset: 0 !important;
        bottom: 0 !important;
        height: 100dvh !important;
        max-height: none !important;
        border-radius: 0 !important;
        padding-bottom: 0 !important;
    }
    body:has(.schematic-viewer.is-fullscreen) .run-bar,
    body:has(.schematic-viewer.is-fullscreen) .status-bar,
    body:has(.flow-diagram.is-fullscreen) .run-bar,
    body:has(.flow-diagram.is-fullscreen) .status-bar {
        display: none !important;
    }
    .schematic-viewer.is-fullscreen .schematic-viewer__toolbar,
    .flow-diagram.is-fullscreen .flow-diagram__head {
        padding-top: calc(var(--space-2) + env(safe-area-inset-top, 0px));
        padding-left: calc(var(--space-2) + env(safe-area-inset-left, 0px));
        padding-right: calc(var(--space-2) + env(safe-area-inset-right, 0px));
    }
}


/* ── Mobile: Bootstrap modals declared inside the dock must escape it ──────
   FilesCard renders <Modal> instances inline (create-file, file details,
   delete-confirm). On phones the dock is `position: fixed; overflow: hidden;
   z-index: var(--z-drawer)` (1020) and so forms a stacking context. Bootstrap
   appends `.modal-backdrop` to <body> at z-index 1050, which then paints on
   top of the entire dock — including the .modal-dialog trapped inside it.
   iOS WebKit additionally clips the position-fixed dialog to the dock's
   overflow box, so the user only sees the dark backdrop with no reachable
   UI to dismiss it.
   When any backdrop is visible, lift the dock above the modal layer and
   drop its overflow clip so the dialog renders normally. */
@media (max-width: 768px) {
    body:has(.modal-backdrop.show) .kmila-dock {
        z-index: calc(var(--z-modal) + 5) !important;
        overflow: visible !important;
    }

    /* Bootstrap's default 0.5 backdrop leaves the page reading through the
       dialog — list items, dates and the footer link stay legible on phones.
       Push opacity higher and add a 2-px blur so the modal isolates cleanly
       on touch viewports while desktop keeps Bootstrap's lighter dim. */
    .modal-backdrop.show {
        --bs-backdrop-opacity: 0.65;
        opacity: 0.65 !important;
        backdrop-filter: blur(2px);
        -webkit-backdrop-filter: blur(2px);
    }
}


/* ── ConfigurationsPill list: roomier delete button ───────────────────────── */
@media (max-width: 640.98px) {
    .kmila-config-pill__panel {
        width: min(360px, 92vw);
        max-width: 92vw;
    }
    .kmila-config-pill__panel .btn-group-sm > .btn {
        min-width: 44px;
        min-height: 44px;
    }
}


/* ── Disable hover-only transforms on touch ───────────────────────────────── */
@media (hover: none) {
    .card.card-interactive:hover,
    .card.card-hoverable:hover,
    .project-card:hover,
    .learn-card:hover {
        transform: none !important;
    }
}


/* ── Honor safe-area-inset on bottom drawer / sticky footers ──────────────── */
/* Desktop has no notch / home-indicator so this is a no-op there. On mobile
   the dock sits above the StatusBar (which already accounts for safe-area
   via its own padding-bottom) — adding the inset here too created an empty
   strip of ~34 px inside the dock under the panel content, so we scope the
   padding to viewports wider than the mobile dock breakpoint. */
@media (min-width: 769px) {
    .kmila-dock {
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }
}


/* ============================================================================
   Practice (/practice and /practice/{id})
   ----------------------------------------------------------------------------
   The catalog (/practice) keeps the standard `.lesson-page` reading-column
   width — it's a cards grid like `/learn`. The exercise workbench
   (/practice/{id}) instead mirrors the Editor: full content width, flex
   column, no narrow cap, so the two-pane layout (statement | editor) has
   room to breathe on wide viewports.
   ============================================================================ */

.practice-view {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
    min-width: 0;
}

.practice-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
    /* `1fr` row tells the grid the explicit row should fill the grid's
       available height. Without this, implicit rows size to content
       (max of items' intrinsic heights) — which means the row stays
       at ~400px even after `min-height` makes the container taller,
       and the editor pane never sees the extra space. */
    grid-template-rows: 1fr;
    gap: var(--space-5);
    align-items: stretch;
    /* Push the GRID CONTAINER up to the viewport's available height so
       the 1fr row above can fill it. The 8rem subtracts the topbar +
       article paddings + practice-page header (rough). The brief column
       will show empty space below its content on short statements;
       that's an acceptable trade for an editor that fills the viewport. */
    min-height: calc(100dvh - var(--topbar-height) - 8rem);
}

@media (max-width: 1024px) {
    .practice-grid {
        grid-template-columns: minmax(0, 1fr);
        /* Mobile / tablet stack: rows AND container size to natural
           content height — the brief shouldn't be taller than its
           markdown when stacked above the editor. */
        grid-template-rows: auto;
        min-height: 0;
    }
}

.practice-pane {
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-1);
    padding: var(--space-4);
    min-width: 0;
}

.practice-pane--brief {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.practice-pane--editor {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: 0;
    overflow: hidden;
}

.practice-editor-frame {
    /* Fills the editor pane's remaining vertical space in the flex column.
       The parent .practice-pane--editor stretches to match the brief column
       via the grid's `align-items: stretch`, so on Web the frame ends up
       exactly matching the card height minus the actions row.
       `min-height: min(320px, 50dvh)` keeps Monaco usable on tiny viewports
       without forcing the frame to overflow a short pane (the previous
       fixed `height: 60vh` and mobile `clamp(260px, 50dvh, 480px)` ignored
       the pane's real height — leaving empty space on Web and overflowing
       on phones). */
    flex: 1 1 auto;
    min-height: min(320px, 50dvh);
    border-bottom: 1px solid var(--border-subtle);
    display: flex;
    overflow: hidden;
}

.practice-monaco {
    flex: 1 1 auto;
    min-width: 0;
    /* BlazorMonaco stamps `monaco-editor-container` on the outer div, which
       picks up the global `height: 60dvh; min-height: 400px` rule meant for
       the standalone Editor page. In PracticeView the editor lives in a flex
       column inside a grid-stretched card, so the fixed dvh height left an
       empty band at the bottom of the card on desktop. Force the practice
       Monaco to fill its frame instead. */
    height: 100% !important;
    min-height: 0 !important;
}

.practice-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    background-color: var(--surface-1);
    align-items: center;
    justify-content: flex-end;
}

.practice-actions__submit {
    margin-left: auto;
}

.practice-section-title {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--fs-base);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    margin: var(--space-3) 0 var(--space-2) 0;
}

.practice-testcase {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3);
    background-color: var(--surface-1);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
}

.practice-testcase__desc {
    color: var(--text-muted);
    margin: 0;
}

.practice-testcase__hidden {
    color: var(--text-muted);
    font-size: var(--fs-sm);
    font-style: italic;
    margin: 0;
}

.practice-test-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--fs-sm);
}

.practice-test-table th,
.practice-test-table td {
    padding: var(--space-1) var(--space-2);
    border-bottom: 1px solid var(--border-subtle);
    text-align: left;
}

.practice-test-table th {
    color: var(--text-muted);
    font-weight: var(--fw-semibold);
    font-size: var(--fs-2xs);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
}

.practice-test-table tr:last-child td {
    border-bottom: 0;
}

.practice-verdict {
    border-radius: var(--radius-md);
    border: 1px solid var(--border-subtle);
    padding: var(--space-3);
    background-color: var(--surface-1);
}

.practice-verdict--ok    { border-color: var(--success); }
.practice-verdict--wa    { border-color: var(--warning); }
.practice-verdict--build { border-color: var(--danger); }
.practice-verdict--rt    { border-color: var(--danger); }
.practice-verdict--to    { border-color: var(--warning); }

.practice-verdict__head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-2);
}

.practice-verdict__time {
    margin-left: auto;
    color: var(--text-muted);
    font-size: var(--fs-sm);
    font-family: var(--font-mono);
}

.practice-verdict__build-error {
    margin: var(--space-2) 0;
    padding: var(--space-2);
    background-color: var(--surface-2);
    border-radius: var(--radius-sm);
}

.practice-verdict__build-error pre {
    margin: var(--space-2) 0 0 0;
    color: var(--text-muted);
    font-size: var(--fs-sm);
    white-space: pre-wrap;
    word-break: break-word;
}

.practice-verdict__tests {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.practice-verdict__tests li {
    display: grid;
    grid-template-columns: auto auto 1fr auto;
    gap: var(--space-2);
    align-items: center;
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
    background-color: var(--surface-2);
    font-size: var(--fs-sm);
}

.practice-verdict__tests li.ok    { border-left: 3px solid var(--success); }
.practice-verdict__tests li.ko    { border-left: 3px solid var(--danger); }

.practice-verdict__tests .t-name { font-family: var(--font-mono); }
.practice-verdict__tests .t-desc {
    margin-left: var(--space-1);
    color: var(--text-muted);
    font-family: var(--font-sans);
}
.practice-verdict__tests .t-detail {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.practice-verdict__tests .t-arrow {
    margin: 0 var(--space-1);
    color: var(--danger);
}
.practice-verdict__tests .t-actual { color: var(--danger); }
.practice-verdict__tests .t-expected { color: var(--success); }
.practice-verdict__tests .t-time {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
}

.practice-reveal {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.practice-test-reveal {
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background-color: var(--surface-1);
    padding: var(--space-2) var(--space-3);
}

.practice-test-reveal summary {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    cursor: pointer;
    font-weight: var(--fw-semibold);
    list-style: none;
}

.practice-test-reveal[open] summary {
    margin-bottom: var(--space-2);
}

/* ── Monaco region lock for M3 (FillGap) ───────────────────────────────────
   Only the BEGIN_EDIT/END_EDIT block is graded — see PracticeEvaluator.
   These classes are applied as Monaco line decorations from
   window.kmilaPracticeApplyLock. The look is purely a hint: server-side
   the splicing always wins regardless of what the user types.            */

.practice-monaco--locked {
    background-color: color-mix(in srgb, var(--surface-3, #888) 18%, transparent);
    opacity: 0.78;
}

.practice-monaco--editable {
    background-color: color-mix(in srgb, var(--kmila-accent, #4f8cff) 12%, transparent);
    box-shadow: inset 3px 0 0 var(--kmila-accent, #4f8cff);
}

/* ── Past-attempts viewer ───────────────────────────────────────────────── */

.practice-history {
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background-color: var(--surface-1);
    padding: var(--space-2) var(--space-3);
}

.practice-history > summary {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    cursor: pointer;
    color: var(--text-muted);
    font-size: var(--fs-sm);
    list-style: none;
}

.practice-history[open] > summary {
    margin-bottom: var(--space-2);
    color: var(--text-strong);
}

.practice-history__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.practice-history__item {
    display: grid;
    grid-template-columns: auto 1fr auto auto;
    gap: var(--space-2);
    align-items: center;
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
    background-color: var(--surface-2);
    font-size: var(--fs-sm);
}

.practice-history__item--ok    { border-left: 3px solid var(--success); }
.practice-history__item--wa    { border-left: 3px solid var(--warning); }
.practice-history__item--build { border-left: 3px solid var(--danger); }
.practice-history__item--rt    { border-left: 3px solid var(--danger); }
.practice-history__item--to    { border-left: 3px solid var(--warning); }

.practice-history__when {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
}

.practice-history__verdict {
    font-weight: var(--fw-semibold);
}

.practice-history__time {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
}

.practice-history__load {
    --bs-btn-padding-x: var(--space-2);
    --bs-btn-padding-y: 2px;
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

/* ── Catalog status chips ──────────────────────────────────────────────── */

.practice-status-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px var(--space-2);
    border-radius: 999px;
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    border: 1px solid transparent;
}

.practice-status-chip--ok {
    background-color: color-mix(in srgb, var(--success) 15%, transparent);
    color: var(--success);
    border-color: color-mix(in srgb, var(--success) 35%, transparent);
}

.practice-status-chip--wip {
    background-color: color-mix(in srgb, var(--warning) 15%, transparent);
    color: var(--warning);
    border-color: color-mix(in srgb, var(--warning) 35%, transparent);
}

.practice-status-chip--todo {
    background-color: var(--surface-2);
    color: var(--text-muted);
    border-color: var(--border-subtle);
}

.practice-status-chip--gated {
    background-color: color-mix(in srgb, var(--text-muted) 12%, transparent);
    color: var(--text-muted);
    border-color: color-mix(in srgb, var(--text-muted) 35%, transparent);
}

/* Gated catalog card — slightly desaturated; hover still works so the
   user can click through to read the explanation banner. */
.learn-card--gated {
    opacity: 0.78;
}
.learn-card--gated:hover {
    opacity: 1;
}

/* Engine-gate banner inside PracticeView. */
.practice-gate-banner {
    display: flex;
    gap: var(--space-3);
    align-items: flex-start;
    padding: var(--space-3);
    border: 1px solid color-mix(in srgb, var(--warning) 40%, transparent);
    background-color: color-mix(in srgb, var(--warning) 10%, transparent);
    border-radius: var(--radius-md);
    color: var(--text-strong);
    margin-bottom: var(--space-3);
}
.practice-gate-banner__icon {
    flex-shrink: 0;
    color: var(--warning);
    font-size: var(--fs-lg);
}
.practice-gate-banner__body strong { display: block; margin-bottom: 4px; }
.practice-gate-banner__body p { margin: 0; }


/* ============================================================================
   Mobile UI bug-fix pass — addresses screenshots reported on 2026-05-08:
     1) Safe-area insets on TopBar + Sidebar drawer (iPhone notch/Dynamic Island).
     2) Profile-menu and ConfigurationsPill popovers — turn into edge-anchored
        sheets on phones so they can never overflow the viewport, and rank
        above the editor dock.
     3) Practice IDE editor frame — drop the 420px floor on phones so Monaco
        fills the visible card without a blank gap below the code.
   (Rendered-markdown table fix lives at line 8996 — `display: table` instead
    of `block`, so the existing `width: 100%` actually stretches the columns.)
   ============================================================================ */

@media (max-width: 640.98px) {

    /* 1) TopBar safe-area is handled by the pre-existing
          `<div class="status-bar-safe-area">` in index.html (styles in
          Kmila.Shared/wwwroot/app.css). Its background was changed to
          --surface-2 to match the TopBar so the two read as a single
          continuous surface. No TopBar override needed here. */

    /* 1) Sidebar drawer — fixed at top:0 with height:100dvh, so border-box
          + padding gives the contents the inset without changing the box. */
    .kmila-sidebar {
        padding-top: env(safe-area-inset-top, 0px);
        padding-bottom: env(safe-area-inset-bottom, 0px);
        box-sizing: border-box;
    }

    /* 2) Both top-bar popovers become full-width sheets pinned under the
          TopBar on phones. This avoids two separate failure modes:
          - Profile menu overflowing the bottom of the viewport and being
            painted under the editor dock.
          - ConfigurationsPill panel extending past the left edge of the
            screen because its right-anchored width was wider than the
            distance from the summary back to x=0. */
    .kmila-profile-menu__panel,
    .kmila-config-pill__panel {
        position: fixed;
        top: calc(var(--topbar-height) + env(safe-area-inset-top, 0px) + var(--space-2));
        right: var(--space-2);
        left: var(--space-2);
        width: auto;
        max-width: none;
        max-height: calc(100dvh - var(--topbar-height) - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - var(--space-4));
        z-index: var(--z-popover);
    }

    [dir="rtl"] .kmila-profile-menu__panel,
    [dir="rtl"] .kmila-config-pill__panel {
        right: var(--space-2);
        left: var(--space-2);
    }

    /* Practice IDE on mobile: rule removed. The new default
       (`flex: 1 1 auto; min-height: min(320px, 50dvh)`) already does the
       right thing on phones — the editor pane has its natural height in
       a single-column layout, the frame fills it, and Monaco respects
       it. The previous fixed `height: clamp(260, 50dvh, 480)` ignored the
       pane's real height and was the source of the overflow the user
       reported on phones + the empty space on Web. */
}

/* ============================================================================
   SchematicViewer — moved here from the (now-empty)
   Kmila.Shared/Components/SchematicViewer.razor.css scoped stylesheet.

   Why the move: MAUI BlazorWebView's static-web-assets pipeline does not
   aggregate referenced RCLs' scoped-CSS bundles into the host's `.styles.css`
   (it shipped only Blazor.Bootstrap's bundle, never Kmila.Shared's). The
   result: the Schemas dock rendered visibly unstyled on Desktop while Web
   was fine. EntityView, ResourceMetricBox, and the button system already
   migrated for the same reason — see the comments at the top of those
   *.razor.css files. Keeping schematic styles in this shared sheet (which
   both hosts already link from <head>) makes them load identically on
   MAUI + Web and survives the scoped-CSS aggregation gap.
   ============================================================================ */

/* Fullscreen mode — pins the viewer to the viewport so a tall schematic can
   be read without fighting the dock height. The toolbar stays at the top so
   the user can keep clicking modes / export / exit-fullscreen while expanded.
   z-index sits below the modal layer so confirm dialogs still pop above. */
.schematic-viewer.is-fullscreen {
    position: fixed;
    inset: 0;
    z-index: calc(var(--z-modal, 1050) - 5);
    background: var(--surface-2);
    border-radius: 0;
    padding: 0;
    margin: 0;
    box-shadow: var(--shadow-3);
}

.schematic-viewer.is-fullscreen .schematic-viewer__body {
    flex: 1 1 auto;
    min-height: 0;
}

/* Visual runtime fullscreen — same fixed-viewport treatment as schematic
   + flow so the canvas gets full-screen real estate. Unlike those two
   the visual panel benefits from breathing room around the canvas so
   dropped widgets don't collide with the phone chrome (notch + home
   indicator) or feel edge-to-edge cramped. Padding uses safe-area
   fallbacks so notched phones don't clip content but larger devices
   still get a comfortable margin. */
.visual-page.is-fullscreen {
    position: fixed;
    inset: 0;
    z-index: calc(var(--z-modal, 1050) - 5);
    background: var(--surface-2);
    box-shadow: var(--shadow-3);
    display: flex;
    flex-direction: column;
    padding-top:    max(env(safe-area-inset-top),    var(--space-4));
    padding-right:  max(env(safe-area-inset-right),  var(--space-4));
    padding-bottom: max(env(safe-area-inset-bottom), var(--space-4));
    padding-left:   max(env(safe-area-inset-left),   var(--space-4));
    gap: var(--space-3);
    /* Small rounded rectangle inside the outer padding so the surface
       reads as a floating card rather than a modal that ate the screen. */
    border-radius: var(--radius-lg, 12px);
}
.visual-page.is-fullscreen .visual-page__workspace {
    flex: 1 1 auto;
    min-height: 0;
    /* Inner card treatment so the workspace surface has its own border +
       rounding inside the outer padding. Keeps the "canvas within a
       framed space" feel while the outer body carries the true margin. */
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md, 8px);
    overflow: hidden;
    background: var(--surface-1);
}
.visual-page.is-fullscreen .visual-page__header,
.visual-page.is-fullscreen .visual-page__toolbar--embedded {
    flex: 0 0 auto;
}

body:has(.visual-page.is-fullscreen) .kmila-dock,
body:has(.visual-page.is-fullscreen) .run-bar,
body:has(.visual-page.is-fullscreen) .status-bar {
    display: none;
}

/* Flow diagram fullscreen — mirrors the schematic block above. The flow
   container itself doesn't have an explicit flex layout (it's a thin wrapper
   around a single .flow-diagram__body), but pinning it to the viewport with
   the same z-index/background rules gives parity with SchematicViewer. */
.flow-diagram.is-fullscreen {
    position: fixed;
    inset: 0;
    z-index: calc(var(--z-modal, 1050) - 5);
    background: var(--surface-2);
    border-radius: 0;
    padding: 0;
    margin: 0;
    box-shadow: var(--shadow-3);
    display: flex;
    flex-direction: column;
}

.flow-diagram.is-fullscreen .flow-diagram__body {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
}

.schematic-viewer {
    --schematic-cell-bg: var(--surface-2);
    --schematic-cell-stroke: var(--text-strong, #1e293b);
    --schematic-cell-fg: var(--text-strong, #1e293b);
    --schematic-group-bg: color-mix(in srgb, var(--kmila-accent) 6%, transparent);
    --schematic-group-stroke: color-mix(in srgb, var(--kmila-accent) 40%, transparent);
    --schematic-debug-fg: var(--success);
    --schematic-select: var(--danger);
    display: flex;
    flex-direction: column;
    gap: .5rem;
    height: 100%;
    width: 100%;
    max-width: 100%;
    min-height: 0;
    min-width: 0;
    overflow: hidden;
    contain: inline-size;
}

.schematic-viewer__toolbar {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .25rem .5rem;
    border-bottom: 1px solid var(--border-subtle, rgba(0,0,0,.08));
    flex-wrap: wrap;
}

.schematic-viewer__title {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
}

.schematic-viewer__chip {
    display: inline-block;
    padding: 0 .4rem;
    border-radius: 999px;
    background: var(--bg-subtle, rgba(0,0,0,.05));
    font-size: var(--fs-xs);
    color: var(--text-muted, #64748b);
}

.schematic-viewer__modes {
    display: inline-flex;
    gap: .25rem;
    margin-left: auto;
}

.schematic-mode {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    background: transparent;
    border: 1px solid transparent;
    color: var(--text-muted, #64748b);
    padding: .15rem .5rem;
    border-radius: 6px;
    font-size: var(--fs-sm);
    cursor: pointer;
}

.schematic-mode.is-active {
    background: var(--bg-subtle, color-mix(in srgb, var(--kmila-accent) 8%, transparent));
    border-color: var(--brand-accent, color-mix(in srgb, var(--kmila-accent) 50%, transparent));
    color: var(--text-strong, #1e293b);
}

.schematic-viewer__actions {
    display: inline-flex;
    gap: .25rem;
}

.schematic-diagnostics {
    list-style: none;
    margin: 0;
    padding: .25rem .5rem;
    display: flex;
    flex-direction: column;
    gap: .15rem;
    font-size: var(--fs-xs);
}

.schematic-diagnostics__item {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    color: var(--text-muted, #64748b);
}

.schematic-diagnostics__item--blackboxprocess {
    color: #b45309;
}

.schematic-diagnostics__item--info {
    color: var(--text-muted, #64748b);
}

.schematic-diagnostics__item--warning {
    color: #b45309;
}

.schematic-diagnostics__item--error {
    color: #b91c1c;
}

.schematic-viewer__body {
    flex: 1 1 0;
    width: 100%;
    max-width: 100%;
    min-height: 0;
    min-width: 0;
    overflow: auto;
    padding: 1rem;
    position: relative;
    contain: inline-size;
}

.schematic-svg {
    display: block;
    cursor: default;
}

.schematic-cell,
.schematic-wire,
.schematic-port {
    transition: opacity .15s ease, stroke-width .15s ease;
    cursor: pointer;
}

.schematic-svg.has-selection .schematic-cell.is-dimmed,
.schematic-svg.has-selection .schematic-wire.is-dimmed,
.schematic-svg.has-selection .schematic-port.is-dimmed {
    opacity: 0.15;
}

.schematic-svg.has-selection .schematic-cell.is-traced,
.schematic-svg.has-selection .schematic-wire.is-traced,
.schematic-svg.has-selection .schematic-port.is-traced {
    opacity: 1;
}

.schematic-cell.is-hidden,
.schematic-wire.is-hidden,
.schematic-port.is-hidden {
    display: none;
}

.schematic-toolbar__count {
    display: inline-block;
    margin-left: .3rem;
    padding: 0 .35rem;
    border-radius: 999px;
    background: #6366f1;
    color: #fff;
    font-size: var(--fs-xs);
    font-weight: 600;
    line-height: 1.4;
    min-width: 1.2rem;
    text-align: center;
}

.schematic-cell.is-multi-selected > path:first-child {
    stroke: #6366f1;
    stroke-width: 2.5;
}
.schematic-wire.is-multi-selected {
    stroke: #6366f1 !important;
    stroke-width: 4 !important;
}
.schematic-port.is-multi-selected circle {
    stroke: #6366f1;
    stroke-width: 3;
}

/* #3 — schematic step-through: the gate whose source line just produced a
   transition pulses; the wires carrying the changed signals glow. Kept above
   the dim/trace rules visually by using !important on stroke so it reads even
   when a trace selection is also active. */
.schematic-cell.is-active-step > path:first-child {
    stroke: var(--accent, #f59e0b) !important;
    stroke-width: 3 !important;
    animation: kmila-schem-pulse 1s ease-in-out infinite;
    transform-box: fill-box;
    transform-origin: center;
}
.schematic-cell.is-active-step {
    opacity: 1 !important;
}
.schematic-wire.is-active-step {
    stroke: var(--accent, #f59e0b) !important;
    stroke-width: 4 !important;
    opacity: 1 !important;
    animation: kmila-schem-wire-pulse 1s ease-in-out infinite;
}
@keyframes kmila-schem-pulse {
    0%, 100% { filter: drop-shadow(0 0 0 transparent); }
    50%      { filter: drop-shadow(0 0 4px var(--accent, #f59e0b)); }
}
@keyframes kmila-schem-wire-pulse {
    0%, 100% { opacity: .65; }
    50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
    .schematic-cell.is-active-step > path:first-child,
    .schematic-wire.is-active-step { animation: none; }
}

/* #3 — live step-through control bar (Debug mode over a running sim). */
.schematic-debug-bar {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-2);
    border-bottom: 1px solid var(--border-subtle);
    font-size: var(--fs-sm);
    flex-wrap: wrap;
}
.schematic-debug-bar__status {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    font-weight: 600;
    color: var(--accent, #f59e0b);
}
.schematic-debug-bar__hint {
    flex: 1 1 12rem;
    min-width: 0;
    color: var(--text-2, #64748b);
}

.schematic-basket {
    position: absolute;
    bottom: 1rem;
    inset-inline-end: 1rem;
    min-width: 240px;
    background: var(--surface-2);
    border: 1px solid var(--border-subtle, rgba(0,0,0,.12));
    border-radius: 8px;
    box-shadow: var(--shadow-2);
    padding: .5rem;
    font-size: var(--fs-sm);
    z-index: 5;
    display: flex;
    flex-direction: column;
    gap: .35rem;
}

.schematic-basket__head {
    display: flex;
    align-items: center;
    gap: .35rem;
    font-family: ui-monospace, Menlo, monospace;
    color: var(--text-strong, #1e293b);
}

.schematic-basket__hint {
    margin-left: auto;
    color: var(--text-muted, #64748b);
    font-weight: 400;
    font-size: var(--fs-xs);
}

.schematic-basket__counts {
    display: flex;
    flex-wrap: wrap;
    gap: .35rem;
}

.schematic-basket__chip {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    padding: .15rem .4rem;
    border-radius: 999px;
    background: rgba(99, 102, 241, 0.12);
    color: var(--text-strong, #1e293b);
    font-family: ui-monospace, Menlo, monospace;
}

.schematic-basket__chip small {
    color: var(--text-muted, #64748b);
}

.schematic-basket__actions {
    display: flex;
    justify-content: flex-end;
    gap: .25rem;
}

.schematic-cell.is-traced > path:first-child {
    stroke-width: 2;
}

.schematic-wire.is-traced {
    stroke-width: 3.5;
}

.schematic-cell.is-selected > path:first-child {
    stroke: var(--schematic-select);
    stroke-width: 3;
}

.schematic-wire.is-selected {
    stroke: var(--schematic-select) !important;
    stroke-width: 4 !important;
}

.schematic-port.is-selected circle {
    stroke: var(--schematic-select);
    stroke-width: 3;
}

.schematic-cell:hover > path:first-child {
    stroke: var(--schematic-select);
}

.schematic-wire:hover {
    stroke: var(--schematic-select);
}

.schematic-info {
    position: absolute;
    top: 1rem;
    inset-inline-end: 1rem;
    width: 320px;
    max-height: calc(100% - 2rem);
    overflow: auto;
    background: var(--surface-2);
    border: 1px solid var(--border-subtle, rgba(0,0,0,.12));
    border-radius: 8px;
    box-shadow: 0 6px 20px rgba(0,0,0,.08);
    padding: .5rem;
    font-size: var(--fs-sm);
    z-index: 5;
}

.schematic-info__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    padding-bottom: .25rem;
    margin-bottom: .25rem;
    border-bottom: 1px solid var(--border-subtle, rgba(0,0,0,.08));
    font-family: ui-monospace, Menlo, monospace;
}

.schematic-info__body {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: .25rem .75rem;
    margin: 0;
}

.schematic-info__body dt {
    color: var(--text-muted, #64748b);
    font-weight: 500;
}

.schematic-info__body dd {
    margin: 0;
    font-family: ui-monospace, Menlo, monospace;
    word-break: break-all;
}

.schematic-info__doc {
    display: flex;
    flex-direction: column;
    gap: .35rem;
    padding-bottom: .35rem;
    margin-bottom: .35rem;
    border-bottom: 1px solid var(--border-subtle, rgba(0,0,0,.08));
}

.schematic-info__doc-summary {
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--text-strong, #1e293b);
    margin: 0;
    line-height: 1.35;
}

.schematic-info__doc-body {
    font-size: var(--fs-xs);
    color: var(--text-muted, #475569);
    margin: 0;
    line-height: 1.45;
}

.schematic-info__truth {
    font-family: ui-monospace, Menlo, monospace;
    font-size: var(--fs-xs);
    border-collapse: collapse;
    margin: .15rem 0;
    width: auto;
    color: var(--text-strong, #1e293b);
}

.schematic-info__truth th,
.schematic-info__truth td {
    padding: 1px 8px;
    border: 1px solid var(--border-subtle, rgba(0,0,0,.12));
    text-align: center;
}

.schematic-info__truth th {
    background: var(--bg-subtle, rgba(0,0,0,.04));
    font-weight: 600;
}

.schematic-info__vhdl {
    font-family: ui-monospace, Menlo, monospace;
    font-size: var(--fs-xs);
    background: var(--bg-subtle, rgba(0,0,0,.04));
    padding: .35rem .5rem;
    border-radius: 4px;
    margin: .1rem 0 0 0;
    overflow-x: auto;
    white-space: pre;
    color: var(--text-strong, #1e293b);
}

.schematic-info__note {
    font-size: var(--fs-xs);
    color: var(--text-muted, #64748b);
    margin: .15rem 0 0 0;
    display: flex;
    gap: .3rem;
    align-items: flex-start;
    line-height: 1.4;
}

.schematic-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    color: var(--text-muted, #64748b);
    gap: .5rem;
}

/* ── Concepts track + ExecutionPlayer ──────────────────────────────────────
   The Concepts mini-curriculum teaches sequential vs concurrent vs why
   "true parallel" doesn't exist in VHDL. Its keystone is the ExecutionPlayer:
   a read-only Monaco editor that highlights one or more lines per step from
   a pre-recorded JSON timeline, with a live signal table and tape transport.

   Highlight class is applied via monaco.editor.deltaDecorations from
   window.kmilaExecHighlightLines (see js/app.js). Keep the rule lightweight
   — Monaco re-applies it on every model change.                            */

.kmila-exec-highlight {
    background-color: color-mix(in srgb, var(--kmila-accent, #4f8cff) 22%, transparent);
    box-shadow: inset 3px 0 0 var(--kmila-accent, #4f8cff);
}

/* Live-step highlight on the main editor's Monaco — warmer hue + the
   same left-bar accent so it visually mirrors the ExecutionPlayer's
   "currently running" tint without looking identical (the editor is
   a richer surface and amber-on-slate reads as "stopped here right now"
   to most users in our school-tested theme). */
.kmila-editor-exec-line {
    background-color: color-mix(in srgb, var(--warning, #f59e0b) 22%, transparent);
    box-shadow: inset 3px 0 0 var(--warning, #f59e0b);
}

/* ExecutionPlayer container. Two-column on desktop, stacks on narrow. */
.kmila-exec-player {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    gap: var(--space-3);
    margin: var(--space-3) 0;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background-color: var(--surface-1);
    padding: var(--space-3);
}

@media (max-width: 880px) {
    .kmila-exec-player {
        grid-template-columns: 1fr;
    }
}

.kmila-exec-player__code {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-width: 0;
}

.kmila-exec-monaco {
    height: 320px;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    overflow: hidden;
}

.kmila-exec-player__side {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-width: 0;
}

.kmila-exec-player__note {
    background-color: color-mix(in srgb, var(--kmila-accent, #4f8cff) 8%, var(--surface-2));
    border-left: 3px solid var(--kmila-accent, #4f8cff);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    font-size: var(--fs-base);
    line-height: 1.4;
    min-height: 4.5em;
}

.kmila-exec-player__note-empty {
    color: var(--text-muted, #64748b);
    font-style: italic;
}

.kmila-exec-signals {
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    background-color: var(--surface-2);
    overflow: hidden;
}

.kmila-exec-signals__header {
    padding: var(--space-2) var(--space-3);
    background-color: var(--surface-3, var(--surface-2));
    font-weight: 600;
    font-size: var(--fs-sm);
    color: var(--text-muted, #64748b);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.kmila-exec-signals__table {
    width: 100%;
    border-collapse: collapse;
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
    font-size: var(--fs-base);
}

.kmila-exec-signals__table th,
.kmila-exec-signals__table td {
    padding: 0.3rem var(--space-3);
    text-align: left;
    border-top: 1px solid var(--border-subtle);
}

.kmila-exec-signals__table th {
    width: 38%;
    color: var(--text-muted, #64748b);
    font-weight: 500;
}

.kmila-exec-signals__value {
    font-weight: 600;
}

/* Soft flash when a signal value changes from the previous step.
   prefers-reduced-motion users get a static highlight color and skip the
   animation entirely so we don't violate the OS accessibility setting. */
.kmila-exec-signals__value--changed {
    background-color: color-mix(in srgb, var(--success, #10b981) 22%, transparent);
    border-radius: 3px;
    padding: 0 .25rem;
    animation: kmila-exec-pulse 700ms ease-out;
}

@keyframes kmila-exec-pulse {
    from { background-color: color-mix(in srgb, var(--success, #10b981) 38%, transparent); }
    to   { background-color: color-mix(in srgb, var(--success, #10b981) 0%, transparent); }
}

@media (prefers-reduced-motion: reduce) {
    .kmila-exec-signals__value--changed {
        animation: none;
    }
}

/* Tape transport: ⏮ ◀ ▶ ⏸ ▶▶ ⟲ + speed picker */
.kmila-exec-tape {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

.kmila-exec-tape__progress {
    flex: 1 1 100%;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--text-muted, #64748b);
    font-size: var(--fs-sm);
}

.kmila-exec-tape__progress-track {
    flex: 1;
    height: 6px;
    background-color: var(--surface-3, var(--surface-2));
    border-radius: 999px;
    overflow: hidden;
}

.kmila-exec-tape__progress-fill {
    height: 100%;
    background: linear-gradient(90deg, var(--kmila-accent, #4f8cff), var(--success, #10b981));
    transition: width 200ms ease-out;
}

.kmila-exec-tape__speed {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    font-size: var(--fs-sm);
    color: var(--text-muted, #64748b);
}

.kmila-exec-tape__speed select {
    background-color: var(--surface-2);
    color: inherit;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    padding: .15rem .4rem;
    font: inherit;
}

/* ── GateAnimator ──────────────────────────────────────────────────────────
   Schematic-style sibling of ExecutionPlayer. SVG circuit on the left, the
   shared signal table + note pane on the right. Reuses .kmila-exec-tape +
   .kmila-exec-signals + .kmila-exec-player__note + .kmila-exec-player__side
   so the two players visually rhyme inside the same lesson page.

   Theme tokens follow the Concepts player: --kmila-accent for "hot" /
   logical-1 propagation, --surface-2 for the cold-wire base. Pulse
   animation is honoured only when prefers-reduced-motion is unset.       */

.kmila-gate-player {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    gap: var(--space-3);
    margin: var(--space-3) 0;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background-color: var(--surface-1);
    padding: var(--space-3);
}

@media (max-width: 880px) {
    .kmila-gate-player { grid-template-columns: 1fr; }
}

.kmila-gate-player__stage {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-width: 0;
}

.kmila-gate-canvas {
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    padding: var(--space-2);
    overflow: hidden;
}

.kmila-gate-canvas svg {
    width: 100%;
    height: auto;
    display: block;
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
}

/* Wires: thin lines that fade between "carrying 0" (cold) and "carrying 1"
   (hot). Each unique source-node id gets its own HSL hue (mirrors the
   SchematicViewer port-colour scheme), applied via an inline style on the
   path element. The cold state ignores the hue and renders muted so the
   "where the 1 is travelling" reading is unambiguous. */
.kmila-gate-wire {
    stroke-width: 2.2;
    stroke-linecap: round;
    stroke-linejoin: round;
    transition: stroke 220ms ease-out, stroke-width 220ms ease-out;
}
.kmila-gate-wire--cold {
    stroke: color-mix(in srgb, var(--text-muted, #64748b) 55%, transparent);
}
.kmila-gate-wire--hot {
    stroke-width: 3.4;
}

/* Gate bodies + terminals share a foreground colour so light/dark themes
   pick up automatically. Active gates pulse the stroke to draw the eye. */
.kmila-gate-body,
.kmila-gate-accent,
.kmila-gate-terminal {
    fill: var(--surface-1);
    stroke: var(--text-muted, #64748b);
    stroke-width: 1.6;
    transition: stroke 200ms ease-out, fill 200ms ease-out;
}
.kmila-gate-accent { fill: none; }
.kmila-gate-bubble {
    fill: var(--surface-1);
    stroke: var(--text-muted, #64748b);
    stroke-width: 1.4;
}

.kmila-gate-terminal--input  { fill: color-mix(in srgb, var(--kmila-accent, #4f8cff) 14%, var(--surface-1)); }
.kmila-gate-terminal--output { fill: color-mix(in srgb, var(--success, #10b981) 14%, var(--surface-1)); }

.kmila-gate-node--active .kmila-gate-body,
.kmila-gate-node--active .kmila-gate-accent {
    stroke: var(--kmila-accent, #4f8cff);
    stroke-width: 2.4;
    filter: drop-shadow(0 0 3px color-mix(in srgb, var(--kmila-accent, #4f8cff) 60%, transparent));
    animation: kmila-gate-pulse 800ms ease-out;
}

@keyframes kmila-gate-pulse {
    0%   { stroke-width: 3.6; }
    100% { stroke-width: 2.4; }
}

/* Spotlight mode: gates not on the live propagation path fade out so the
   firing gate + its hot wires read at a glance. Input/output terminals
   never get this class (they stay as layout anchors). Opt-in via
   doc.spotlight in the .gates.json sidecar. */
.kmila-gate-node--dimmed {
    opacity: 0.22;
    transition: opacity 220ms ease-out;
}

.kmila-gate-label {
    font-size: var(--fs-xs);
    font-weight: 600;
    fill: var(--text-muted, #64748b);
}

/* Small in-body schematic-style pin hints (D, CLK, R, Q, EN) on DFF /
   latch / tristate primitives. Lighter than the gate label so they sit
   visibly inside the body without competing with the gate's identity. */
.kmila-gate-pin-label {
    font-size: 9.5px;
    font-weight: 600;
    fill: var(--text-muted, #64748b);
    opacity: 0.7;
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
}

/* Junction dots — small filled circles at wire branch points. Auto-detected
   for any from-spec shared by 2+ wires, plus any manual entries in
   Doc.Junctions. Default colour matches the cold-wire muted token so the
   dot reads even on a 0-carrying trunk; the inline style swaps in the
   source's hue when that trunk is hot. */
.kmila-gate-junction {
    fill: color-mix(in srgb, var(--text-muted, #64748b) 60%, transparent);
    stroke: var(--surface-1);
    stroke-width: 1;
    transition: fill 220ms ease-out, stroke 220ms ease-out;
}

/* Timing strip — rendered above the main canvas when Doc.ClockSignal is
   set. Thin clock-waveform diagram with a vertical cursor at the current
   step; anchors "this is the rising edge" pedagogically for sequential
   lessons. */
.kmila-gate-timing {
    background-color: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    padding: var(--space-1) var(--space-2);
    margin-bottom: var(--space-2);
    overflow-x: auto;
}
.kmila-gate-timing__svg {
    height: 36px;
    width: 100%;
    min-width: 320px;
}
.kmila-gate-timing__label {
    font-size: var(--fs-2xs);
    font-weight: 600;
    fill: var(--text-muted, #64748b);
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
}
.kmila-gate-timing__wave {
    stroke: var(--kmila-accent, #4f8cff);
    stroke-width: 2;
    stroke-linejoin: miter;
    fill: none;
}
.kmila-gate-timing__cursor {
    stroke: color-mix(in srgb, var(--success, #10b981) 70%, transparent);
    stroke-width: 1.4;
    stroke-dasharray: 2 2;
}

/* Keyboard-shortcut hint chip in the tape transport. Hidden on small
   screens (mobile users don't have physical keyboards), kept terse so it
   doesn't fight for attention with the play / step buttons. */
.kmila-exec-tape__hint {
    margin-left: var(--space-2);
    font-size: var(--fs-xs);
    color: var(--text-muted, #64748b);
    opacity: 0.7;
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
    white-space: nowrap;
}
@media (max-width: 720px) {
    .kmila-exec-tape__hint { display: none; }
}

/* Mobile: tall diagrams (logic-gates is 720×760) eat the whole viewport on
   a phone. Cap the canvas at 80vh and let the user scroll vertically; also
   allow horizontal scroll for super-wide layouts. The viewBox keeps the
   diagram readable, just no longer requires a full screen of vertical
   space before the side panel becomes visible. */
.kmila-gate-canvas {
    max-height: 80vh;
    overflow: auto;
}

/* ── FsmAnimator ───────────────────────────────────────────────────────────
   Third archetype, sibling to GateAnimator. State circles + transition
   arrows; the host frame (.kmila-gate-player + .kmila-gate-canvas + the
   tape + signal table) is shared, so only the SVG primitives need their
   own classes here. */

.kmila-fsm-state__body {
    fill: var(--surface-1);
    stroke: var(--text-muted, #64748b);
    stroke-width: 1.8;
    transition: stroke 200ms ease-out, fill 200ms ease-out;
}
.kmila-fsm-state__label {
    font-size: var(--fs-xs);
    font-weight: 600;
    fill: var(--text);
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
}
.kmila-fsm-state__moore {
    font-size: var(--fs-2xs);
    fill: var(--text-muted, #64748b);
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
}
.kmila-fsm-state--active .kmila-fsm-state__body {
    stroke: var(--kmila-accent, #4f8cff);
    stroke-width: 3;
    fill: color-mix(in srgb, var(--kmila-accent, #4f8cff) 14%, var(--surface-1));
    animation: kmila-gate-pulse 800ms ease-out;
}

.kmila-fsm-transition {
    stroke: color-mix(in srgb, var(--text-muted, #64748b) 55%, transparent);
    stroke-width: 1.6;
    fill: none;
    transition: stroke 200ms ease-out, stroke-width 200ms ease-out;
}
.kmila-fsm-transition--active {
    stroke: var(--kmila-accent, #4f8cff);
    stroke-width: 2.4;
}
.kmila-fsm-arrow-head {
    fill: color-mix(in srgb, var(--text-muted, #64748b) 60%, transparent);
}
.kmila-fsm-arrow-head--active {
    fill: var(--kmila-accent, #4f8cff);
}
.kmila-fsm-transition__label {
    font-size: var(--fs-2xs);
    font-weight: 500;
    fill: var(--text-muted, #64748b);
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
}
.kmila-fsm-transition__label--active {
    fill: var(--kmila-accent, #4f8cff);
    font-weight: 600;
}

@media (prefers-reduced-motion: reduce) {
    .kmila-fsm-state--active .kmila-fsm-state__body { animation: none; }
}

/* ── Step replay panel ─────────────────────────────────────────────────────
   Phase A added the panel itself (kmila-replay / kmila-replay-empty); Phase B
   added the live branch (kmila-replay--live) with pause/resume/step controls
   above the inner ExecutionPlayer. Styles below cover both branches with the
   same visual language — header chip + control row + the player below. */

.kmila-replay,
.kmila-replay-empty {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3);
}

.kmila-replay__header {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--fs-base);
    color: var(--text-muted, #64748b);
}

.kmila-replay__title {
    font-weight: 600;
    color: var(--text);
}

.kmila-replay__meta {
    margin-left: auto;
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
    font-size: var(--fs-sm);
}

.kmila-replay--live .kmila-replay__header {
    color: color-mix(in srgb, var(--success, #10b981) 70%, var(--text-muted, #64748b));
}

.kmila-replay__live-controls {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background-color: color-mix(in srgb, var(--success, #10b981) 8%, var(--surface-2));
    border: 1px solid color-mix(in srgb, var(--success, #10b981) 30%, var(--border-subtle));
    border-radius: var(--radius-sm);
    flex-wrap: wrap;
}

/* Live-control buttons (Resume / Pause / Step Forward) must keep their
   icon + label inline. `.kmila-btn` uses `overflow: hidden` so wrapped
   text would clip — `nowrap` keeps the label on one line, `flex-shrink: 0`
   stops a narrow parent from squeezing the button below its content
   width. Applied to both the schematic debug bar and the replay panel
   live row so the same buttons read identically in both surfaces. */
.kmila-replay__live-controls > .kmila-btn,
.schematic-debug-bar > .kmila-btn {
    white-space: nowrap;
    flex-shrink: 0;
}

.kmila-replay__live-hint {
    margin-left: auto;
    font-size: var(--fs-sm);
    color: var(--text-muted, #64748b);
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
}

.kmila-replay-empty {
    text-align: center;
    color: var(--text-muted, #64748b);
}
.kmila-replay-empty__icon {
    font-size: var(--fs-2xl);
    opacity: 0.6;
}
.kmila-replay-empty h3 {
    margin: 0;
    font-size: var(--fs-base);
    color: var(--text);
}
.kmila-replay-empty p {
    margin: 0;
    font-size: var(--fs-base);
}

/* Phase C — detail-level selector chip in the post-hoc header. */
.kmila-replay__detail {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    margin-left: var(--space-2);
    font-size: var(--fs-sm);
    color: var(--text-muted, #64748b);
}
.kmila-replay__detail select {
    background-color: var(--surface-2);
    color: inherit;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    padding: .15rem .4rem;
    font: inherit;
}

/* Phase E — line-breakpoint editor inside the live panel. */
.kmila-replay__breakpoints {
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    background-color: var(--surface-2);
    padding: var(--space-2) var(--space-3);
}
.kmila-replay__breakpoints summary {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    cursor: pointer;
    font-size: var(--fs-base);
    color: var(--text);
    list-style: none;
}
.kmila-replay__breakpoints summary::-webkit-details-marker { display: none; }
.kmila-replay__bp-count {
    margin-left: auto;
    font-size: var(--fs-xs);
    color: var(--text-muted, #64748b);
    padding: 0 .5rem;
    background-color: var(--surface-3, var(--surface-2));
    border-radius: 999px;
}
.kmila-replay__bp-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: var(--space-2) 0;
}
.kmila-replay__bp-input {
    flex: 1;
    min-width: 0;
    padding: .3rem .6rem;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    background-color: var(--surface-1);
    color: var(--text);
    font: inherit;
}
.kmila-replay__bp-empty {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--text-muted, #64748b);
    font-style: italic;
}
.kmila-replay__bp-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: .25rem;
}
.kmila-replay__bp-list li {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: .3rem .6rem;
    background-color: var(--surface-1);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
    font-size: var(--fs-sm);
}
.kmila-replay__bp-list li span { flex: 1; }

/* Untracked breakpoint warning (#4): the line maps to no signal change, so
   the engine can never pause there. Mute the row + show a warning glyph. */
.kmila-replay__bp-list li.is-untracked { opacity: .72; }
.kmila-replay__bp-list li .kmila-replay__bp-warn {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    color: var(--warning, #f59e0b);
}

/* ── Phase D: signal-table filter UI ──────────────────────────────────────
   Surfaces only on big designs (≥12 signals with ≥2 distinct port types).
   Search box on top, port-type chips + group-by checkbox underneath.       */

.kmila-exec-signals__filter {
    display: flex;
    flex-direction: column;
    gap: .35rem;
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
    background-color: color-mix(in srgb, var(--surface-2) 50%, var(--surface-1));
}

.kmila-exec-signals__search {
    width: 100%;
    padding: .25rem .5rem;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    background-color: var(--surface-1);
    color: var(--text);
    font-size: var(--fs-sm);
}

.kmila-exec-signals__chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .25rem;
}

.kmila-exec-signals__chip {
    border: 1px solid var(--border-subtle);
    background-color: var(--surface-1);
    color: var(--text-muted, #64748b);
    border-radius: 999px;
    padding: .1rem .55rem;
    font-size: var(--fs-xs);
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
    cursor: pointer;
    transition: background-color 160ms ease-out, color 160ms ease-out, border-color 160ms ease-out;
}
.kmila-exec-signals__chip:hover { color: var(--text); }
.kmila-exec-signals__chip--active {
    background-color: color-mix(in srgb, var(--kmila-accent, #4f8cff) 18%, var(--surface-1));
    border-color: var(--kmila-accent, #4f8cff);
    color: var(--kmila-accent, #4f8cff);
}

.kmila-exec-signals__groupby {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    font-size: var(--fs-xs);
    color: var(--text-muted, #64748b);
    cursor: pointer;
    user-select: none;
}

/* ── Monaco gutter breakpoint glyph (Phase B+ follow-up) ──────────────────
   Painted by kmilaSetMonacoBreakpoints via Monaco's glyphMarginClassName.
   Sits in the editor's glyph margin to the LEFT of the line number.

   Why a pseudo-element instead of a background gradient: Monaco sizes the
   overlay div to the line height (~18–22 px tall) but lets the glyph
   className control the visible content. A radial-gradient with no fixed
   height stretches into an ellipse on a tall line. A centered ::after
   pseudo with explicit dimensions renders a real circle every time. */
.kmila-bp-glyph {
    position: relative;
    cursor: pointer;
}
.kmila-bp-glyph::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--danger, #ef4444);
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--danger, #ef4444) 45%, transparent);
}
.kmila-bp-glyph:hover::after {
    background: color-mix(in srgb, var(--danger, #ef4444) 80%, white);
}

/* Untracked breakpoint (#4): the line maps to no signal assignment, so the
   engine can never pause there. Render it hollow + muted so it reads as
   "set but inert" rather than a live red dot. The hover tooltip (set in
   kmilaSetMonacoBreakpoints) explains why. */
.kmila-bp-glyph--untracked::after {
    background: transparent;
    border: 1.5px dashed var(--text-3, #9ca3af);
    box-shadow: none;
    width: 9px;
    height: 9px;
}
.kmila-bp-glyph--untracked:hover::after {
    background: color-mix(in srgb, var(--text-3, #9ca3af) 25%, transparent);
}

/* ── Editor card: Code / Step Replay tab strip (Phase B+ follow-up 3) ─────
   Promoted out of the bottom dock so the debug surface sits right next to
   the source it's stepping through. Uses the same .kmila-btn-tab family
   the dock uses so the visual language stays consistent. */
.editor-workbench__view-tabs {
    display: flex;
    gap: var(--space-2, 0.5rem);
    padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem) 0;
    border-bottom: 1px solid var(--border-subtle);
    background-color: var(--surface-2);
    /* `.editor-workbench` is a flex column and `.editor-workbench__body--panes`
       declares `min-height: 320px` on mobile. Without an explicit shrink:0
       guard the tab strip collapses to 0 height when Monaco's body demands its
       min — the tabs visibly disappear right when an actual file is loaded.
       Pinning shrink:0 keeps the tab strip always rendered. */
    flex-shrink: 0;
    /* EW-1 (editor-mobile audit 2026-06-29): four tabs (Code · Blocks ·
       Step replay · Visual) overflow a 1080-px phone. Make the strip
       horizontally scrollable with a soft right-edge fade so the rightmost
       tab is discoverable. Tabs themselves must not shrink — otherwise
       they collapse below their icon+label intrinsic size. */
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 24px), transparent 100%);
            mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 24px), transparent 100%);
}
.editor-workbench__view-tabs::-webkit-scrollbar { display: none; }
.editor-workbench__view-tabs .kmila-btn-tab { flex-shrink: 0; }
/* EW-1: at extreme narrow widths drop the labels so all 4 tabs fit
   without forcing a horizontal scroll the user might not discover. Bump
   icon size + per-button padding so the tap target stays ≥ 44 px square
   even when the label is gone. */
@media (max-width: 420px) {
    .editor-workbench__view-tabs .kmila-btn-tab > span:not(.editor-workbench__view-tab-badge) {
        display: none;
    }
    .editor-workbench__view-tabs .kmila-btn-tab {
        min-width: 48px;
        min-height: 44px;
        padding: var(--space-2) var(--space-3);
        justify-content: center;
        font-size: 1.125rem; /* scales the inner <i class="bi"> icon */
    }
    /* Distribute the 4 tabs across the available width on icon-only mode
       so they don't crowd into the left edge with empty space on the right. */
    .editor-workbench__view-tabs { justify-content: space-around; gap: 0; }
}
.editor-workbench__view-tabs .kmila-btn-tab {
    position: relative;
}
.editor-workbench__view-tabs .kmila-btn-tab[disabled] {
    opacity: 0.45;
    cursor: not-allowed;
}
.editor-workbench__view-tab-badge {
    margin-left: .35rem;
    font-size: var(--fs-xs);
    line-height: 1;
}
.editor-workbench__view-tab-badge--live {
    color: var(--success, #10b981);
    animation: kmila-view-tab-pulse 1200ms ease-in-out infinite;
}
.editor-workbench__view-tab-badge--paused {
    color: var(--warning, #f59e0b);
}
/* Editor sync mode (2026-07-06) — stale-preview pip on the non-active
   Code/Blocks tab when Manual sync mode is active and the source tab has
   unsynced edits. Reuses the badge geometry; slate accent so it doesn't
   compete with the live/paused chips. */
.editor-workbench__view-tab-badge--stale {
    color: var(--warning, #f59e0b);
    font-size: calc(var(--fs-xs) * 1.1);
}
@keyframes kmila-view-tab-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.45; }
}
@media (prefers-reduced-motion: reduce) {
    .editor-workbench__view-tab-badge--live { animation: none; }
}

.editor-workbench__view {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}
.editor-workbench__view--code {
    /* Inherits the existing workbench-header + body-panes layout. The
       inner children already have their own flex sizing, so this wrapper
       just needs to keep the flex chain going. */
}
.editor-workbench__view--replay {
    overflow: auto;
}

/* Subgroup header row when "Group by type" is on. */
.kmila-exec-signals__group-row th {
    padding: .3rem var(--space-3);
    font-size: var(--fs-xs);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted, #64748b);
    background-color: var(--surface-2);
    border-top: 1px solid var(--border-subtle);
}

.kmila-gate-chip rect {
    fill: var(--surface-3, var(--surface-2));
    stroke: var(--border-subtle);
    stroke-width: 1;
    transition: fill 220ms ease-out, stroke 220ms ease-out;
}
.kmila-gate-chip text {
    font-size: var(--fs-xs);
    font-weight: 700;
    fill: var(--text-muted, #64748b);
    font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
    transition: fill 220ms ease-out;
}
.kmila-gate-chip--hot rect {
    fill: color-mix(in srgb, var(--kmila-accent, #4f8cff) 22%, var(--surface-1));
    stroke: var(--kmila-accent, #4f8cff);
}
.kmila-gate-chip--hot text { fill: var(--kmila-accent, #4f8cff); }

.kmila-gate-node--changed .kmila-gate-chip rect {
    animation: kmila-gate-chip-pulse 700ms ease-out;
}

@keyframes kmila-gate-chip-pulse {
    from { stroke-width: 2.4; }
    to   { stroke-width: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .kmila-gate-node--active .kmila-gate-body,
    .kmila-gate-node--active .kmila-gate-accent,
    .kmila-gate-node--changed .kmila-gate-chip rect {
        animation: none;
    }
}

/* ── Concepts Scratch-style exercise board ─────────────────────────────────
   ConceptExercise.razor wraps a Blockly workspace + actions + the same
   ExecutionPlayer used by the lessons (matched timeline). The Blockly
   workspace is mounted by JS, so .kmila-block-editor__canvas needs an
   explicit height — Blockly otherwise collapses to 0px and stays empty. */

.kmila-exercise-board {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin-top: var(--space-3);
}

.kmila-block-editor {
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    overflow: hidden;
    background-color: var(--surface-1);
}

.kmila-block-editor__canvas {
    /* The workspace is the whole point of the exercise view — give it real
       viewport budget. Reasoning: lesson chrome (crumb + title + chips +
       brief) above and the Run/Reset row + match banner below together
       eat ~25vh; what's left should be the workspace. Bumped 2026-05-28
       after Daniel noted the previous 760px cap still felt cramped. */
    height: 88vh;
    min-height: 640px;
    width: 100%;
}

@media (max-width: 720px) {
    .kmila-block-editor__canvas {
        height: 78vh;
        min-height: 480px;
    }
}

/* Phase G (2026-06-28): inside the Editor's Blocks tab the workspace is
   wrapped by an .editor-workbench__view--blocks flex cell instead of the
   Concepts exercise board, so the 88vh fixed height above overflows the
   smaller editor card. In this context the BlockEditor flex-fills its
   parent column, and the canvas grows naturally. The bottom-sheet palette
   from .kmila-block-root then has real estate to actually surface on
   phone widths instead of being pushed off-screen by an 88vh canvas. */
.editor-workbench__view--blocks .kmila-block-editor {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
}
.editor-workbench__view--blocks .kmila-block-editor__canvas {
    flex: 1 1 auto;
    height: auto;
    min-height: 360px;
}
/* B-3 (editor-mobile audit 2026-06-29 — partial accept): on phone widths,
   give the canvas a sensible viewport-relative cap so it doesn't leave a
   huge empty band BELOW it on tall screens. The earlier attempt to chain
   `flex-basis: 0` collapsed the JS engine's bounding-rect read at init
   (engine saw a 0-height canvas → blocks rendered off-screen). Capping
   the inner max-height lets the canvas grow to most-of-viewport instead
   of all-of-viewport, accepting a small empty band rather than breaking
   block rendering. */
@media (max-width: 768px) {
    .editor-workbench__view--blocks .kmila-block-editor__canvas {
        /* 2026-08-08 mobile round 11 — Fix 5: reserve room for the new
           category-tabs palette strip (~7 rem: 34 px tab row + 60 px chip
           row + 12 px padding + separator) so the palette actually
           surfaces above the run bar instead of getting pushed off-screen
           by an over-tall canvas. Previous 9 rem subtraction did not
           account for the taller palette. */
        min-height: calc(100dvh
            - var(--topbar-height, 56px)
            - var(--dock-h, 64px)
            - var(--runbar-h, 56px)
            - env(safe-area-inset-top, 0px)
            - env(safe-area-inset-bottom, 0px)
            - 16rem /* view-tabs + editor-card padding + palette pill+chip rows */);
    }
}

.kmila-exercise-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}
/* Phase I (2026-06-28): on narrow phone widths, force Reiniciar +
   Ejecutar to share one row of equal-width pills so the Run target is
   always thumb-reachable from either hand without horizontal scroll. */
.kmila-exercise-actions > .kmila-btn { flex: 1 1 160px; min-width: 0; }
@media (max-width: 480px) {
    .kmila-exercise-actions > .kmila-btn { flex: 1 1 calc(50% - var(--space-2)); }
}

/* Phase I (2026-06-28): verdict / match banner gets the Glass-Slab
   treatment — Family-11 4 px inline-start accent strip in the verdict
   colour, surface-2 card body with shadow-1, slot-grammar internal
   spacing. Subtle slide-up entrance respects prefers-reduced-motion. */
.kmila-exercise-match {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    border: 1px solid var(--border-default);
    border-inline-start-width: 4px;
    border-radius: var(--radius-md);
    padding: var(--space-3) var(--space-4);
    background-color: var(--surface-2);
    box-shadow: var(--shadow-1);
    animation: kmila-exercise-match-in 220ms ease-out;
}

.kmila-exercise-match > div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.kmila-exercise-match strong { display: block; line-height: 1.3; font-size: var(--fs-md); color: var(--text-strong); }
.kmila-exercise-match span { color: var(--text-muted, #64748b); font-size: var(--fs-sm); }
.kmila-exercise-match > .bi,
.kmila-exercise-match > i[class^="bi-"] { font-size: 1.5rem; flex: 0 0 auto; line-height: 1; margin-top: 2px; }

.kmila-exercise-match--ok {
    border-color: color-mix(in srgb, var(--success, #10b981) 35%, var(--border-default));
    border-inline-start-color: var(--success, #10b981);
    background-color: color-mix(in srgb, var(--success, #10b981) 6%, var(--surface-2));
}
.kmila-exercise-match--ok strong { color: var(--success, #10b981); }
.kmila-exercise-match--ok > .bi,
.kmila-exercise-match--ok > i[class^="bi-"] { color: var(--success, #10b981); }

.kmila-exercise-match--miss {
    border-color: color-mix(in srgb, var(--warning, #f59e0b) 35%, var(--border-default));
    border-inline-start-color: var(--warning, #f59e0b);
    background-color: color-mix(in srgb, var(--warning, #f59e0b) 6%, var(--surface-2));
}
.kmila-exercise-match--miss strong { color: var(--warning, #f59e0b); }
.kmila-exercise-match--miss > .bi,
.kmila-exercise-match--miss > i[class^="bi-"] { color: var(--warning, #f59e0b); }

@keyframes kmila-exercise-match-in {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
    .kmila-exercise-match { animation: none; }
}

/* ── kmila-blocks engine — Glass Slabs (2026-06-28) ─────────────────────────
   DOM-based block renderer for /editor (Blocks tab) + /concepts/exercise/*.
   Blocks are frosted-glass slabs on the slate surface palette. Role-color
   lives in a 3 px inline-start accent strip (CSS ::before) + an optional
   type chip — never as a flood-fill background, so identifier text stays
   dark-on-light at 0.82 rem (legible on a phone). Five role tints map to
   the existing --signal-* tokens so the design system stays one source of
   truth — no hard-coded indigo/sky/emerald/amber palette anymore. Dark
   mode propagates automatically via [data-bs-theme="dark"]. */

.kmila-block-root {
    display: flex;
    height: 100%;
    width: 100%;
    background-color: var(--surface-1);
    overflow: hidden;
    /* Role tints — derived from the design-system tokens. The original
       indigo/sky/emerald/amber palette is gone. Each family now resolves
       through one of the brand variables so a single theme update flips
       every block at once. */
    --kmila-block-step:    var(--signal-input);     /* control flow */
    --kmila-block-gate:    var(--signal-vector);    /* combinational */
    --kmila-block-signal:  var(--signal-clock);     /* data flow */
    --kmila-block-clock:   var(--signal-output);    /* clock + IO ports */
    --kmila-block-accent:  var(--kmila-accent);     /* assignments */
    --kmila-block-raw:     var(--slate-500);        /* raw fallback */
}

.kmila-block-palette {
    width: 168px;
    flex: 0 0 168px;
    background-color: var(--surface-2);
    border-right: 1px solid var(--border-subtle);
    padding: var(--space-3);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: stretch;
}
/* 2026-08-08 mobile round 11 — Fix 2: category-tab pill row + chip
   strip wrappers rendered by kmila-blocks.js _renderPalette. On desktop
   the pill row is hidden (the vertical sidebar already lists all
   categories inline via .kmila-toolbox-category); the strip inherits
   the palette's vertical flex flow. On mobile both switch to horizontal
   scrollers — see @media (max-width: 640.98px) below. */
.kmila-block-palette__tabs { display: none; }
.kmila-block-palette__strip {
    display: flex;
    flex-direction: column;
    gap: 8px;
    align-items: stretch;
    min-width: 0;
}

/* Phone: palette swaps to a bottom strip so the canvas keeps the full
   viewport width. Previously 132 px side-rail stole 35 % of a 375 px
   screen, leaving 210 px usable canvas — narrower than a typical port
   row. With the strip pattern blocks can be thumbed through horizontally
   while the canvas stays writable. */
@media (max-width: 640.98px) {
    .kmila-block-root {
        flex-direction: column;
        /* On phone widths the parent (editor card body) sizes itself by
           content, which collapses .kmila-block-root to ~one-block height
           and pushes the bottom-sheet palette off-screen. Pin a minimum
           working height so the canvas + palette strip both fit. */
        min-height: 360px;
    }
    .kmila-block-palette {
        width: 100%;
        flex: 0 0 auto;
        flex-direction: column;
        border-right: 0;
        border-top: 1px solid var(--border-subtle);
        padding: 6px var(--space-2) var(--space-2);
        overflow-x: hidden;
        overflow-y: hidden;
        gap: 0;
        align-items: stretch;
        order: 2;
        box-shadow: 0 -4px 12px rgba(15, 23, 42, 0.06);
        /* 2026-08-08 mobile round 11 — Fix 3: no more max-height clip.
           The palette now hosts a category-tab pill row + a single
           horizontally-scrolling chip strip. That fits in ~112 px of
           intrinsic height without truncating tall block previews. */
    }
    /* Category-tab pill row (rendered by kmila-blocks.js _renderPalette).
       Horizontal, sticky-ish above the block chip strip, brand-tinted
       active state. */
    .kmila-block-palette__tabs {
        display: flex;
        flex: 0 0 auto;
        gap: 8px;
        margin-bottom: 8px;
        overflow-x: auto;
        overflow-y: hidden;
        scrollbar-width: none;
        /* Fix 8: soft snap points so a swipe eases onto a pill instead
           of stopping mid-gap; proximity (not mandatory) so a decisive
           swipe past two pills still lands where the user expects. */
        scroll-snap-type: x proximity;
        scroll-padding-inline: 8px;
        scroll-behavior: smooth;
    }
    .kmila-block-palette__tabs::-webkit-scrollbar { display: none; }
    .kmila-block-palette__tab {
        appearance: none;
        -webkit-appearance: none;
        border: 1px solid var(--border-default);
        background-color: var(--surface-1);
        color: var(--text-default);
        border-radius: 999px;
        padding: 6px 14px;
        font: 500 var(--fs-xs) inherit;
        white-space: nowrap;
        cursor: pointer;
        transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
        min-height: 36px;
        scroll-snap-align: start;
    }
    .kmila-block-palette__tab[aria-selected="true"] {
        background-color: var(--kmila-accent);
        color: var(--surface-1);
        border-color: var(--kmila-accent);
    }
    /* Chip strip: horizontally-scrolling row of the active category's
       block chips. This is the ONLY thing that scrolls horizontally
       now — the tab row above it lists the six categories so students
       don't hunt through 60+ blocks in one endless strip. Override the
       desktop rule that lays the strip out as a vertical stack. */
    .kmila-block-palette__strip {
        display: flex;
        flex-direction: row;
        align-items: flex-start;
        flex: 1 1 auto;
        gap: 10px;
        overflow-x: auto;
        overflow-y: hidden;
        padding: 2px 4px 6px;
        min-height: 0;
        /* Fix 8: same soft snap the tab row uses — chips ease onto a
           natural stop. `scroll-padding-inline: 12px` keeps the first
           chip from jamming against the palette's inline-start edge. */
        scroll-snap-type: x proximity;
        scroll-padding-inline: 12px;
        scroll-behavior: smooth;
        /* Momentum + hide the native scrollbar; users know from the
           tab row that this strip scrolls. */
        scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
    }
    .kmila-block-palette__strip::-webkit-scrollbar { display: none; }
    /* Chip children participate in scroll snap. Also switch touch-action
       so the browser handles the palette's horizontal scroll — the drag
       logic still fires on vertical/large-displacement gestures via the
       Fix 7 movement threshold. */
    .kmila-block-palette__strip > .kmila-block--palette {
        scroll-snap-align: start;
        touch-action: pan-x;
    }
    /* Cap the whole palette so it doesn't overflow the editor's
       overflow:hidden container. ~140 px leaves room for the tab pill
       row (~38 px) + one row of chips (~68 px) + padding, and keeps
       the canvas the majority of the viewport. */
    .kmila-block-palette {
        max-height: 140px;
    }
    /* Category headers rendered inline by _renderPaletteItem become
       redundant once the tab row exists — hide on phone. Separators too. */
    .kmila-block-palette .kmila-toolbox-category,
    .kmila-block-palette .kmila-block-palette__sep { display: none; }
    /* Non-active category chips get `data-palette-visible="false"` from
       the tab-click handler in kmila-blocks.js. Using a bespoke data-
       attribute instead of `hidden`/`aria-hidden` so desktop (where all
       chips must always show) is unaffected by the same JS state. */
    .kmila-block-palette[data-mobile-cat] .kmila-block--palette[data-palette-visible="false"] {
        display: none !important;
    }
    .kmila-block-canvas {
        order: 1;
        flex: 1 1 0;
        min-height: 0;        /* override desktop 100% so palette gets its row */
    }
}
@media (max-width: 720px) and (min-width: 640.99px) {
    .kmila-block-palette { width: 140px; flex-basis: 140px; padding: var(--space-2); }
}

.kmila-block-palette__sep {
    width: 100%;
    height: 1px;
    background-color: var(--border-subtle);
    margin: var(--space-1) 0;
}
/* In bottom-sheet mode the separator becomes a vertical rule between
   category groups instead of a horizontal one. */
@media (max-width: 640.98px) {
    .kmila-block-palette__sep {
        width: 1px;
        height: auto;
        min-height: 32px;
        margin: 0 var(--space-1);
    }
}

/* 2026-07-20 Phase 23 — categoryToolbox headers rendered by
   kmila-blocks.js `_renderPaletteItem` when it encounters
   `item.kind === 'category'`. Small, muted label + top border acts as
   both section header AND visual divider (the sep between groups is
   implicit — the header itself supplies the top rule). Uses shared
   design tokens so light+dark themes get symmetrical treatment. */
.kmila-toolbox-category {
    width: 100%;
    padding: var(--space-2) 0 var(--space-1);
    margin-top: var(--space-2);
    border-top: 1px solid var(--border-subtle);
    color: var(--fg-muted);
    font-size: var(--font-size-xs);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    /* First-of-type: no top border/margin — the very first category
       reads as the palette's own top edge without an extra rule. */
}
.kmila-toolbox-category:first-child {
    border-top: none;
    margin-top: 0;
    padding-top: 0;
}
/* Mobile bottom-sheet layout: headers become inline chips between
   horizontally-scrolling block groups. */
@media (max-width: 640.98px) {
    .kmila-toolbox-category {
        flex: 0 0 auto;
        width: auto;
        border-top: none;
        border-left: 1px solid var(--border-subtle);
        padding: 0 var(--space-2);
        margin: 0 var(--space-1);
        writing-mode: vertical-rl;
        transform: rotate(180deg);
        white-space: nowrap;
    }
    .kmila-toolbox-category:first-child {
        border-left: none;
        padding-left: 0;
    }
}

.kmila-block-canvas {
    flex: 1 1 auto;
    position: relative;
    overflow: auto;
    /* 2026-08-08 mobile round 11 — Fix 5: allow two-finger + one-finger
       pan through long block stacks. Blocks inside still carry
       `touch-action:none` (see .kmila-block) so drag detection works;
       the canvas surface between blocks gets pan-x pan-y so students
       can scroll a 2 645 px stack on a 582 px viewport. */
    touch-action: pan-x pan-y;
    background-image:
        radial-gradient(circle, color-mix(in srgb, var(--border-subtle) 60%, transparent) 1px, transparent 1px);
    background-size: 20px 20px;
    background-position: 0 0;
    min-height: 100%;
    /* Make scrollbar visible to hint at overflow when blocks extend past
       the canvas edge (live-observed: long port decls like
       `data_in : in STD_LOGIC_VECTOR(3 downto 0)` push blocks off-screen on
       phone). Mobile WebKit auto-hides scrollbars on touch devices, so we
       also add an explicit right padding so off-screen content has visual
       slack — the rightmost block can sit inside the padding band without
       its label getting flush-cut by the canvas edge. */
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
    padding: var(--space-2) var(--space-5) var(--space-5) var(--space-2);
}
.kmila-block-canvas::-webkit-scrollbar { height: 8px; width: 8px; }
.kmila-block-canvas::-webkit-scrollbar-thumb {
    background: var(--border-strong);
    border-radius: 999px;
}
.kmila-block-canvas::-webkit-scrollbar-track {
    background: transparent;
}

/* 2026-08-08 mobile round 11 — canvas mobile overrides. This block
   MUST live after the desktop `.kmila-block-canvas` rule above; the
   earlier @media at ~line 12980 gets overwritten by the desktop rule
   at ~line 13051 (same specificity, later wins in cascade). Placing
   the mobile version here lets it actually beat the desktop values. */
@media (max-width: 640.98px) {
    .kmila-block-canvas {
        order: 1;
        flex: 1 1 0;
        min-height: 0;
    }
}

.kmila-block-trash {
    position: absolute;
    /* RTL-aware. Bottom respects safe-area-inset-bottom so it doesn't sit
       under the iOS home-indicator on phones. Size bumped to 56 px (was
       44 px) — visual hit target reads as ~30 px on Android because of the
       dashed border inset; 56 px closes the gap to Material's 48 px min. */
    bottom: max(var(--space-3), env(safe-area-inset-bottom, 0px));
    inset-inline-end: var(--space-3);
    width:  56px;
    height: 56px;
    background-color: var(--surface-2);
    border: 1px solid var(--border-default);
    border-radius: 50%;
    display: grid;
    place-items: center;
    color: var(--text-muted);
    pointer-events: none;
    z-index: 50;
    box-shadow: var(--shadow-2);
    transition: transform 120ms ease, color 120ms ease, border-color 120ms ease, background-color 120ms ease;
}

.kmila-block-trash--active {
    transform: scale(1.15);
    color: var(--danger);
    border-color: var(--danger);
    background-color: color-mix(in srgb, var(--danger) 12%, var(--surface-2));
}

/* ── Block atoms — Glass-Slab style ─────────────────────────────────────── */

.kmila-block {
    display: inline-flex;
    flex-direction: column;
    box-sizing: border-box;
    gap: 6px;
    border-radius: var(--radius-md);
    /* Extra inline-start padding leaves room for the accent strip ::before */
    padding: 8px 12px 8px 18px;
    /* Frosted glass body — surface-2 at 86 % over the canvas dot grid */
    background-color: color-mix(in srgb, var(--surface-2) 86%, transparent);
    -webkit-backdrop-filter: blur(8px) saturate(140%);
    backdrop-filter: blur(8px) saturate(140%);
    border: 1px solid var(--border-default);
    color: var(--text-strong);
    font: 500 0.92rem/1.25 inherit;
    box-shadow: var(--shadow-2);
    user-select: none;
    /* 2026-08-08 mobile round 11 (Fix 9) — idle blocks let the browser
       pan the canvas/scroll the palette. Only after the block has been
       ACTIVATED via double-tap (see .kmila-block--active) do we flip
       touch-action to `none` so the JS drag path takes over. Result:
       a single-finger swipe scrolls the workspace naturally, and
       students commit to a move with a deliberate double-tap. */
    touch-action: pan-x pan-y;
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    cursor: pointer;
    transition: box-shadow 120ms ease, transform 120ms ease, opacity 120ms ease;
    vertical-align: middle;
    position: relative;
    --kmila-block-tint: var(--slate-500);
}

/* Inline-start accent strip — the only chromatic affordance for role.
   Sits inside the padding gutter so it doesn't move layout. */
.kmila-block::before {
    content: "";
    position: absolute;
    inset: 8px auto 8px 6px;
    width: 3px;
    border-radius: 999px;
    background: var(--kmila-block-tint);
}

.kmila-block:focus-visible {
    outline: 2px solid var(--border-focus);
    outline-offset: 2px;
}

.kmila-block:active,
.kmila-block.kmila-block--dragging {
    cursor: grabbing;
}

@media (max-width: 640.98px) {
    .kmila-block { padding: 8px 10px 8px 16px; font-size: 0.86rem; gap: 4px; max-width: 100%; }
    .kmila-block__statement { margin-inline-start: 12px; padding: 4px 6px; }
    /* 2026-08-08 mobile round 11 — Fix 1: mobile inset numbers match the
       reduced .kmila-block padding of `8px 10px 8px 16px`, so chained
       peers still land flush with the head block's inline-start edge. */
    .kmila-block__next-slot { margin-inline-start: -16px; margin-inline-end: -10px; }
    /* Fix 4: rows must actually wrap on narrow canvases. `flex-wrap:wrap`
       is inherited from the desktop rule; the problem is that inputs have
       `flex:1 1 6ch` which lets them shrink but not stack. Force each
       text/dropdown to claim its own line once we're under 641 px so
       long identifiers like `data_in : std_logic_vector` render fully
       instead of clipping. Labels stay inline (they hug the field). */
    .kmila-block__row { row-gap: 6px; }
    .kmila-block-canvas > .kmila-block .kmila-block__field--text,
    .kmila-block-canvas > .kmila-block .kmila-block__field--dropdown,
    .kmila-block__statement .kmila-block__field--text,
    .kmila-block__statement .kmila-block__field--dropdown,
    .kmila-block__next-slot .kmila-block__field--text,
    .kmila-block__next-slot .kmila-block__field--dropdown {
        flex-basis: 100%;
        min-width: 0;
    }
}

.kmila-block--dragging {
    /* Dragging always takes the pointer — no browser pan/scroll while
       a drag is in flight. */
    touch-action: none;
    box-shadow:
        var(--shadow-3),
        inset 0 0 0 1px color-mix(in srgb, var(--border-focus) 60%, transparent);
    outline: 1px solid color-mix(in srgb, var(--kmila-accent) 50%, transparent);
    outline-offset: -1px;
    transform: rotate(-0.8deg);
    opacity: 0.96;
    z-index: 20;
}

/* 2026-08-08 mobile round 11 (Fix 9) — "picked up" / active block.
   Reached via a double-tap; hosts the drag capture until dropped. */
.kmila-block--active {
    touch-action: none;
    cursor: grab;
    outline: 2px solid var(--kmila-accent);
    outline-offset: 2px;
    box-shadow:
        var(--shadow-3),
        0 0 0 4px color-mix(in srgb, var(--kmila-accent) 22%, transparent);
    animation: kmila-block-active-pulse 1.4s ease-in-out infinite alternate;
}
@keyframes kmila-block-active-pulse {
    from { box-shadow: var(--shadow-3), 0 0 0 4px color-mix(in srgb, var(--kmila-accent) 22%, transparent); }
    to   { box-shadow: var(--shadow-3), 0 0 0 6px color-mix(in srgb, var(--kmila-accent) 12%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
    .kmila-block--active { animation: none; }
}

.kmila-block--over-trash {
    opacity: 0.55;
    transform: scale(0.92) rotate(-2deg);
}

.kmila-block--palette { cursor: grab; transform: none; }
.kmila-block--palette select,
.kmila-block--palette .kmila-block__field--dropdown { pointer-events: none; }
.kmila-block--palette:hover {
    transform: translateY(-1px);
    box-shadow: var(--shadow-3);
}
/* 2026-08-08 mobile round 11 — Fix 3: palette blocks in phone bottom-strip
   render as CHIPS. Statement slots, statement labels, and the next-slot
   are decorative — they clip the 96 px strip and confuse the student.
   Show only the first (name) row of the block so students see WHAT they
   are picking up. Full block materializes normally on the canvas after
   drop (_onPointerDown clones a fresh Block that does NOT carry the
   -chip class). */
.kmila-block--palette-chip {
    align-self: flex-start;
    max-width: 220px;
}
.kmila-block--palette-chip .kmila-block__statement,
.kmila-block--palette-chip .kmila-block__statement-label,
.kmila-block--palette-chip .kmila-block__next-slot {
    display: none;
}
/* Palette chips also don't need internal-child fill or long text inputs
   to expand — cap the field width so a chip stays a chip. */
.kmila-block--palette-chip .kmila-block__row {
    flex-wrap: nowrap;
}
.kmila-block--palette-chip .kmila-block__field--text,
.kmila-block--palette-chip .kmila-block__field--dropdown {
    max-width: 12ch;
    flex: 0 1 auto;
}

/* Family tints — strip + chip color only, no background fill */
.kmila-block--step    { --kmila-block-tint: var(--kmila-block-step);    }
.kmila-block--gate    { --kmila-block-tint: var(--kmila-block-gate);    }
.kmila-block--signal  { --kmila-block-tint: var(--kmila-block-signal);  }
.kmila-block--clock   { --kmila-block-tint: var(--kmila-block-clock);   }
.kmila-block--accent  { --kmila-block-tint: var(--kmila-block-accent);  }

/* Raw-VHDL fallback — slate background, dashed border, mono text. The
   strip ::before still renders in slate so the visual rhythm is kept. */
.kmila-block--raw {
    --kmila-block-tint: var(--kmila-block-raw);
    background-color: color-mix(in srgb, var(--surface-3) 92%, transparent);
    border: 1px dashed var(--border-strong);
}
.kmila-block--raw .kmila-block__field--label {
    font: 600 var(--fs-2xs) var(--font-mono);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
}
.kmila-block--raw .kmila-block__field--text {
    font-family: var(--font-mono);
    font-size: 0.82rem;
    background-color: var(--surface-1);
}

/* Optional type-chip badge — uppercase mono, takes its colour from --tint.
   Rendered only if the JS catalog adds a `<span class="kmila-block__type">`
   element; otherwise the strip alone communicates role. */
.kmila-block__type {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font: 600 var(--fs-2xs) var(--font-mono);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--kmila-block-tint);
}
.kmila-block__type::before {
    content: "";
    width: 8px; height: 8px;
    border-radius: 2px;
    background: var(--kmila-block-tint);
}

/* Simulator error decoration — keep red glow + 2-cycle pulse, swap the
   hard-coded rgba for design-system --danger. */
.kmila-block--error {
    outline: 2px solid var(--danger);
    outline-offset: 1px;
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 25%, transparent);
    animation: kmila-block-error-pulse 1.2s ease-out 2;
}
@keyframes kmila-block-error-pulse {
    0%   { box-shadow: 0 0 0 0   color-mix(in srgb, var(--danger) 55%, transparent); }
    100% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 25%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
    .kmila-block--error { animation: none; }
}

/* Value blocks (output-bearing expression atoms) — pill shape, tinted
   border + soft tinted background so they still read as inline values. */
.kmila-block--has-output {
    border-radius: 999px;
    padding: 6px 12px 6px 14px;
    background-color: color-mix(in srgb, var(--kmila-block-tint) 12%, var(--surface-2));
    border-color: var(--kmila-block-tint);
    color: var(--kmila-block-tint);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: var(--shadow-1);
}
.kmila-block--has-output::before { display: none; }
.kmila-block--has-output.kmila-block--gate {
    border-radius: var(--radius-md);
    padding: 6px 10px;
}

/* Phase A.5 fit fix: rows behave as full-width flex containers so inputs
   share remaining horizontal space evenly and wrap (not clip) on narrow
   canvases. Closes the std_logic_vector dropdown-clip bug. */
.kmila-block__row {
    display: flex;
    flex-wrap: wrap;
    width: 100%;
    min-width: 0;
    gap: 6px 8px;
    align-items: center;
}
/* Nested blocks fill their slot; top-level blocks fill the canvas. */
.kmila-block__statement > .kmila-block,
.kmila-block-canvas > .kmila-block { width: 100%; }

.kmila-block__field--label {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    white-space: nowrap;
    flex: 0 0 auto;
}

/* Field inputs — Kmila form chrome (surface-1 background + slate border)
   instead of translucent white-on-tint. Inputs flex with min-width:0 so
   they shrink on narrow widths instead of overflowing. */
.kmila-block__field--dropdown,
.kmila-block__field--text {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background-color: var(--surface-1);
    color: var(--text-strong);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-xs);
    padding: 4px 8px;
    font: inherit;
    font-size: var(--fs-xs);
    min-width: 0;
    flex: 1 1 6ch;
    min-height: 2em;
}
.kmila-block__field--dropdown {
    padding-right: 22px;
    cursor: pointer;
    background-image:
        linear-gradient(45deg, transparent 50%, var(--text-muted) 50%),
        linear-gradient(135deg, var(--text-muted) 50%, transparent 50%);
    background-position:
        calc(100% - 12px) calc(50% - 1px),
        calc(100% - 7px)  calc(50% - 1px);
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
}
.kmila-block__field--dropdown option {
    color: var(--text-default);
    background-color: var(--surface-1);
}
.kmila-block__field--dropdown:focus-visible,
.kmila-block__field--text:focus-visible {
    outline: 2px solid var(--border-focus);
    outline-offset: 1px;
}
.kmila-block__field--text::placeholder { color: var(--text-subtle); }

@media (max-width: 640.98px) {
    .kmila-block__field--text { min-height: 2.4em; padding: 6px 10px; }
    .kmila-block__field--dropdown { min-height: 2.4em; padding: 6px 22px 6px 8px; }
}

/* Slot labels — promoted to first-class uppercase mono chips so "then" /
   "else" / "begin" / "declarations" stop being illegible 0.66 rem
   afterthoughts. Sits inline-start of the slot it labels. */
.kmila-block__statement-label {
    display: inline-flex;
    align-self: flex-start;
    font: 600 var(--fs-2xs) var(--font-mono);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
    background-color: var(--surface-1);
    padding: 2px 6px;
    border-radius: var(--radius-xs);
    margin-bottom: 4px;
}

/* ── Slots (value / statement / next) ─────────────────────────────────── */

.kmila-slot {
    box-sizing: border-box;
    transition: background-color 120ms ease, outline-color 120ms ease;
}

.kmila-block__value {
    display: inline-flex;
    align-items: center;
    min-width: 38px;
    min-height: 22px;
    padding: 0;
    border-radius: 999px;
    background-color: color-mix(in srgb, var(--surface-1) 90%, transparent);
    outline: 1px dashed var(--border-default);
    outline-offset: -1px;
    vertical-align: middle;
}
.kmila-block__value:not(:empty) {
    background-color: transparent;
    outline: none;
    min-width: 0;
    min-height: 0;
    padding: 0;
}

.kmila-block__statement {
    margin-top: 4px;
    margin-inline-start: 12px;
    padding: 6px 8px;
    min-height: 28px;
    background-color: color-mix(in srgb, var(--surface-1) 92%, transparent);
    border-radius: var(--radius-sm);
    border: 1px dashed var(--border-default);
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    max-height: 60vh;
    overflow-y: auto;
    overflow-x: visible;
}
.kmila-block__statement:not(:empty) {
    border-style: solid;
    border-color: var(--border-subtle);
    padding: 6px;
}

.kmila-block__next-slot {
    min-height: 0;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    /* 2026-08-08 mobile round 11 — Fix 1: cancel the parent block's inline
       padding so chained peers (port→port→port, signal→signal, case-when
       chains…) align back to the head block's inline-start edge instead
       of cascading right one nesting level at a time. The parent's
       padding is `8px 12px 8px 18px` (see .kmila-block); mobile drops it
       to `8px 10px 8px 16px` inside the ≤640px media query. */
    margin-inline-start: -18px;
    margin-inline-end:   -12px;
}

/* Chained peers fill the head block's full width (matches the same rule
   that already exists for .kmila-block__statement > .kmila-block and
   .kmila-block-canvas > .kmila-block at ~line 13279). */
.kmila-block__next-slot > .kmila-block { margin-top: 6px; width: 100%; }
.kmila-block__statement > .kmila-block { margin-top: 0; }
.kmila-block__statement > .kmila-block + .kmila-block { margin-top: 4px; }

/* Snap preview — accent ring + brand-tinted fill. White-on-tint stopped
   making sense once block bodies are light frosted glass. */
.kmila-slot--snap {
    outline: 2px solid var(--kmila-accent) !important;
    outline-offset: 0;
    background-color: color-mix(in srgb, var(--kmila-accent) 18%, var(--surface-1)) !important;
    animation: kmila-slot-pulse 600ms ease-in-out infinite alternate;
}

@keyframes kmila-slot-pulse {
    from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--kmila-accent) 55%, transparent); }
    to   { box-shadow: 0 0 0 6px color-mix(in srgb, var(--kmila-accent) 0%, transparent); }
}

/* ── Dark-mode polish ──────────────────────────────────────────────────
   Token-driven surfaces flip automatically; we only need to lift the
   dot grid contrast and tighten the slab body alpha so the frosted-glass
   blur reads on the slate-900 canvas without bleeding into invisibility. */

[data-bs-theme="dark"] .kmila-block-canvas {
    background-image:
        radial-gradient(circle, color-mix(in srgb, var(--border-subtle) 80%, transparent) 1px, transparent 1px);
}

[data-bs-theme="dark"] .kmila-block {
    background-color: color-mix(in srgb, var(--surface-2) 78%, transparent);
}

[data-bs-theme="dark"] .kmila-block-trash {
    background-color: var(--surface-3, var(--surface-2));
}

/* ── Reduced-motion respect ───────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
    .kmila-block,
    .kmila-block-trash,
    .kmila-slot {
        transition: none;
    }
    .kmila-slot--snap {
        animation: none;
    }
    .kmila-block--dragging {
        transform: none;
    }
}

/* ── Phase G (2026-06-28) — floating toolbar ──────────────────────────
   Pill of icon-buttons docked inside the BlockEditor card's top-inline-
   end. Sits on top of the canvas, stays reachable as the student scrolls
   through a long stack. Touch sizing follows Family-14 icon-button policy
   (32 px desktop / 40 px on phone). */
.kmila-block-editor { position: relative; }

.kmila-block-toolbar {
    position: absolute;
    top: var(--space-3);
    inset-inline-end: var(--space-3);
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 4px;
    background-color: var(--surface-2);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-2);
    z-index: 60;
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
}
.kmila-block-toolbar__btn {
    appearance: none; -webkit-appearance: none;
    background: transparent;
    border: 0;
    width: 32px; height: 32px;
    border-radius: 999px;
    color: var(--text-default);
    cursor: pointer;
    display: inline-grid;
    place-items: center;
    font-size: 0.95rem;
    transition: background-color 120ms ease, color 120ms ease, transform 120ms ease;
}
.kmila-block-toolbar__btn:hover { background-color: var(--surface-3); }
.kmila-block-toolbar__btn:active { transform: scale(0.92); }
.kmila-block-toolbar__btn:focus-visible {
    outline: 2px solid var(--border-focus);
    outline-offset: 2px;
}
.kmila-block-toolbar__btn--danger { color: var(--danger); }
.kmila-block-toolbar__btn--danger:hover {
    background-color: color-mix(in srgb, var(--danger) 12%, var(--surface-2));
}
.kmila-block-toolbar__sep {
    display: inline-block;
    width: 1px; height: 20px;
    background-color: var(--border-subtle);
    margin: 0 4px;
}
@media (max-width: 640.98px) {
    /* 2026-08-08 mobile round 11 — Fix 6: on phones the top-anchored
       toolbar sat directly on top of the first block, obscuring its
       right edge. Re-anchor to bottom-start so it lives above the
       palette strip and out of the way of block content. Bottom
       distance = 140 px palette max-height + 8 px gap + safe-area. */
    .kmila-block-toolbar {
        top: auto;
        bottom: calc(148px + env(safe-area-inset-bottom, 0px));
        inset-inline-start: var(--space-2);
        inset-inline-end: auto;
    }
    .kmila-block-toolbar__btn { width: 40px; height: 40px; font-size: 1.05rem; }
}

/* Long-press affordance — toggled by the kmila-blocks engine on touch
   pointerdown for a brief 100-150 ms grace period before the drag
   actually starts, confirming "I'm picking it up" to the user. */
.kmila-block--pressing {
    transform: scale(1.04);
    box-shadow:
        var(--shadow-3),
        0 0 0 3px color-mix(in srgb, var(--kmila-accent) 35%, transparent);
}
@media (prefers-reduced-motion: reduce) {
    .kmila-block--pressing { transform: none; box-shadow: var(--shadow-3); }
}

/* ── End-of-lesson micro-quiz ───────────────────────────────────────────── */

.kmila-quizlet {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background-color: var(--surface-1);
    padding: var(--space-3);
    margin-top: var(--space-3);
}

.kmila-quizlet__header {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    color: var(--text-muted, #64748b);
    text-transform: uppercase;
    font-size: var(--fs-sm);
    letter-spacing: 0.06em;
    font-weight: 600;
}

.kmila-quizlet__question {
    margin: 0;
    font-size: var(--fs-md);
    font-weight: 500;
    line-height: 1.4;
}

.kmila-quizlet__options {
    display: flex;
    flex-direction: column;
    gap: var(--space-1, .4rem);
    border: 0;
    padding: 0;
    margin: 0;
}

.kmila-quizlet__option {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    background-color: var(--surface-2);
    cursor: pointer;
    transition: background-color 120ms ease, border-color 120ms ease;
}

.kmila-quizlet__option:hover {
    border-color: color-mix(in srgb, var(--kmila-accent, #4f8cff) 40%, var(--border-subtle));
}

.kmila-quizlet__option input { margin-top: .25rem; }

.kmila-quizlet__option--correct {
    border-color: color-mix(in srgb, var(--success, #10b981) 60%, var(--border-subtle));
    background-color: color-mix(in srgb, var(--success, #10b981) 10%, var(--surface-2));
}

.kmila-quizlet__option--wrong {
    border-color: color-mix(in srgb, var(--danger, #ef4444) 60%, var(--border-subtle));
    background-color: color-mix(in srgb, var(--danger, #ef4444) 10%, var(--surface-2));
}

.kmila-quizlet__feedback {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    margin-top: var(--space-2);
}

.kmila-quizlet__feedback strong { display: block; margin-bottom: .2rem; }
.kmila-quizlet__feedback p { margin: 0; line-height: 1.4; }

.kmila-quizlet__feedback--ok {
    border-color: color-mix(in srgb, var(--success, #10b981) 60%, var(--border-subtle));
    background-color: color-mix(in srgb, var(--success, #10b981) 8%, var(--surface-2));
}

.kmila-quizlet__feedback--miss {
    border-color: color-mix(in srgb, var(--danger, #ef4444) 60%, var(--border-subtle));
    background-color: color-mix(in srgb, var(--danger, #ef4444) 8%, var(--surface-2));
}


/* ============================================================================
   Welcome tour (kmila-tutorial.js) — first-run coachmark overlay.
   Sits above modals but below toasts so a toast can still surface during
   the tour if something asynchronous fires.
   ============================================================================ */

.kmila-tour {
    position: fixed;
    inset: 0;
    z-index: calc(var(--z-toast) - 1);
    pointer-events: none; /* children opt back in */
}

.kmila-tour__backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
    pointer-events: auto;
    animation: kmila-tour-fade-in 200ms var(--ease-standard, ease-out);
}

.kmila-tour__spotlight {
    position: absolute;
    pointer-events: none;
    border-radius: var(--radius-md, 8px);
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55);
    transition: top 200ms ease, left 200ms ease, width 200ms ease, height 200ms ease;
}

.kmila-tour__card {
    position: absolute;
    pointer-events: auto;
    background: var(--surface-1, #fff);
    color: var(--text-strong, #111);
    border: 1px solid var(--border-default, rgba(0,0,0,0.15));
    border-radius: var(--radius-md, 8px);
    box-shadow: var(--shadow-3, 0 12px 32px rgba(0,0,0,0.25));
    padding: var(--space-4, 16px);
    transition: top 200ms ease, left 200ms ease;
    max-width: calc(100vw - 24px);
}

.kmila-tour__step {
    font-size: var(--fs-xs, 11px);
    color: var(--text-muted, #666);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: var(--space-2, 8px);
}

.kmila-tour__title {
    font-size: var(--fs-lg, 18px);
    font-weight: 600;
    margin: 0 0 var(--space-2, 8px);
}

.kmila-tour__body {
    font-size: var(--fs-sm, 14px);
    line-height: 1.5;
    margin: 0 0 var(--space-3, 12px);
    color: var(--text-default, #222);
}

.kmila-tour__actions {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
}

.kmila-tour__spacer { flex: 1; }

.kmila-tour__btn {
    border: 1px solid var(--border-default, rgba(0,0,0,0.15));
    background: var(--surface-2, #f5f5f5);
    color: var(--text-strong, #111);
    border-radius: var(--radius-sm, 6px);
    padding: 6px 12px;
    font-size: var(--fs-sm, 14px);
    cursor: pointer;
    transition: background 120ms ease, border-color 120ms ease;
}

.kmila-tour__btn:hover { background: var(--surface-3, #ececec); }

.kmila-tour__btn--ghost {
    background: transparent;
    border-color: transparent;
    color: var(--text-muted, #555);
}

.kmila-tour__btn--ghost:hover {
    background: var(--surface-2, #f5f5f5);
    color: var(--text-strong, #111);
}

.kmila-tour__btn--primary {
    background: var(--kmila-primary, #4f46e5);
    border-color: var(--kmila-primary, #4f46e5);
    color: #fff;
}

.kmila-tour__btn--primary:hover {
    background: var(--kmila-primary-dark, #4338ca);
    border-color: var(--kmila-primary-dark, #4338ca);
}

@keyframes kmila-tour-fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .kmila-tour__backdrop { animation: none; }
    .kmila-tour__spotlight, .kmila-tour__card { transition: none; }
}

/* Mobile: shrink the card and center it instead of side-anchoring. */
@media (max-width: 640.98px) {
    .kmila-tour__card {
        left: 12px !important;
        right: 12px !important;
        width: auto !important;
        max-width: calc(100vw - 24px);
    }
}

/* ---- Project templates gallery (TemplateGalleryModal) ---- */

.kmila-template-gallery__subtitle {
    color: var(--text-muted);
    font-size: var(--fs-sm);
    margin: 0 0 var(--space-4) 0;
}

.kmila-template-grid {
    display: grid;
    /* 160px min was too tight: longest-locale template names
       ("Máquina de estados Moore" / "Maschine endlicher Zustände") truncated
       at 4 columns. Bumping to 220px so the grid auto-fits at most 3 columns
       on the modal's ~870px content width, leaving room for full names + a
       3-line description. */
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}

/* Single-column on narrow screens. Lang-grid stays compact because chips are
   small; template cards have a description line and stack better. */
@media (max-width: 640.98px) {
    .kmila-template-grid {
        grid-template-columns: 1fr;
    }
}

/* Template card: visuals delegated to .kmila-btn-tab. Local class only
   reshapes the layout to icon-on-top + name + description column. */
.kmila-template-card {
    flex-direction: column !important;
    align-items: flex-start !important;
    gap: var(--space-1) !important;
    padding: var(--space-3) !important;
    min-height: 112px;
    text-align: start;
    white-space: normal;
}

.kmila-template-card__icon {
    font-size: calc(22px * var(--user-text-scale, 1));
    color: var(--accent-fg);
    margin-bottom: var(--space-1);
}

.kmila-template-card__name {
    font-weight: var(--fw-semibold);
    font-size: var(--fs-md);
    line-height: 1.25;
}

.kmila-template-card__desc {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    line-height: 1.35;
    /* `white-space: normal` overrides the parent button's `nowrap` default
       so the description wraps inside the card column instead of overflowing
       horizontally into adjacent cards. Without this, line-clamp can never
       trigger (no line breaks) and the text streams past the right edge. */
    white-space: normal;
    width: 100%;
    display: block;
    overflow: hidden;
    /* Allow up to 3 lines of description before truncating. */
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
}

.kmila-template-card.is-active .kmila-template-card__icon,
.kmila-template-card.is-active .kmila-template-card__desc {
    color: var(--text-default);
}

/* Skeleton tiles while the manifest loads. */
.kmila-template-card--skeleton {
    pointer-events: none;
    background: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
}

.kmila-template-gallery__empty {
    display: flex;
    gap: var(--space-2);
    align-items: center;
    color: var(--text-muted);
    padding: var(--space-4) 0;
}

/* Form below the grid: project name input + optional checkbox. */
.kmila-template-form {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
}

.kmila-template-form__check {
    display: flex;
    gap: var(--space-2);
    align-items: flex-start;
    padding: var(--space-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: var(--surface-2);
    cursor: pointer;
}

.kmila-template-form__check input[type="checkbox"] {
    flex: 0 0 auto;
    margin-top: 2px;
}

.kmila-template-form__check small {
    display: block;
    color: var(--text-muted);
    font-size: var(--fs-xs);
    margin-top: 2px;
}

.kmila-template-form__actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
}

/* ── Visual runtime mode (Phase 0) ─────────────────────────────────── */
.visual-page {
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
    max-width: 100%;
    background: var(--surface-1);
    /* Defensive isolation: even if a widget/panned canvas paints outside
       its scroll container, it must not push the whole page (html/body have
       overflow-x: hidden as a global backstop; this pins the visual panel
       to its allotted column). */
    overflow-x: clip;
}

.visual-page__header {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
    background: var(--surface-2);
}

.visual-page__title {
    display: flex;
    flex-direction: column;
    line-height: 1.2;
}

.visual-page__project {
    font-weight: 600;
}

.visual-page__entity {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
}

.visual-page__toolbar {
    display: flex;
    gap: var(--space-2);
    /* Fill the parent row (header) so the internal
       `.kmila-slot-toolbar-divider--pusher` has slack to consume — the old
       `margin-left: auto` here shrink-wrapped the whole toolbar to its
       content and killed the inner spacer. `min-width: 0` prevents flex-child
       overflow when the row is tight. In the embedded case the toolbar is a
       block-level child that already spans 100 % of its parent, so the
       `flex` property is inert there but harmless. */
    flex: 1 1 auto;
    min-width: 0;
    flex-wrap: wrap;
    align-items: center;
}

/* Custom hover tooltip that reads `data-tip`. Native `title` tooltips are
   inconsistent across the MAUI WebView + Blazor Server hosts and are invisible
   on touch, so icon-only buttons in the visual toolbar carry both:
     - data-tip → styled bubble (below), delayed to avoid flicker
     - title    → native fallback + accessibility tree
   The bubble sits above the button with a small arrow and is theme-aware. It
   never intercepts pointer events so it can't block the click it advertises. */
.kmila-tip { position: relative; }
.kmila-tip[data-tip]::after {
    content: attr(data-tip);
    position: absolute;
    left: 50%;
    bottom: calc(100% + 8px);
    transform: translateX(-50%) translateY(2px);
    padding: 4px 8px;
    /* The `--surface-inverse` / `--text-inverse` tokens are a paired inverse
       (light-mode: dark surface + light text; dark-mode: light surface + dark
       text) — kept together so contrast survives both themes. An earlier
       version used `--text-on-inverse` here, which the token system doesn't
       define, so it fell back to a hard-coded near-white and rendered
       invisible on dark mode (light text on the now-light inverse surface). */
    background: var(--surface-inverse);
    color: var(--text-inverse);
    font-size: var(--fs-xs);
    line-height: 1.2;
    font-family: var(--font-sans);
    font-weight: var(--fw-medium, 500);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-2);
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    z-index: var(--z-tooltip, 1200);
    transition: opacity var(--duration-fast) var(--ease-standard) 0ms,
                transform var(--duration-fast) var(--ease-standard) 0ms;
}
.kmila-tip[data-tip]:hover::after,
.kmila-tip[data-tip]:focus-visible::after {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
    /* Small delay so quick pointer sweeps across the toolbar don't strobe. */
    transition-delay: 250ms;
}
/* When a button is right at the toolbar edge the centered bubble could clip;
   the parent overflow guards let it escape the flex row. */
.visual-page__toolbar { overflow: visible; }
/* Touch devices don't fire hover reliably — suppress the bubble so it doesn't
   linger after a tap on iOS/Android. `title` still exists for long-press. */
@media (hover: none) {
    .kmila-tip[data-tip]::after { display: none; }
}

/* "More" overflow dropdown — houses secondary view actions so the toolbar
   stays legible when a viewport can't fit every button on one row. The
   trigger sits inline with the other icon buttons; the panel floats below
   it. A transparent backdrop catches outside clicks. */
.visual-page__more-wrap {
    position: relative;
    display: inline-flex;
}
.visual-page__more-backdrop {
    position: fixed;
    inset: 0;
    z-index: calc(var(--z-popover, 1100) - 1);
    background: transparent;
}
.visual-page__more-menu {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    z-index: var(--z-popover, 1100);
    min-width: 200px;
    max-width: min(280px, calc(100vw - 16px));
    padding: 4px;
    background: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-3);
    display: flex;
    flex-direction: column;
    gap: 2px;
    animation: kmilaPop var(--duration-fast) var(--ease-standard);
}
[dir="rtl"] .visual-page__more-menu { right: auto; left: 0; }
.visual-page__more-item {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    background: none;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--text-default);
    font-family: inherit;
    font-size: var(--fs-sm);
    text-align: left;
    cursor: pointer;
    white-space: nowrap;
    transition: background-color var(--duration-fast) var(--ease-standard);
}
[dir="rtl"] .visual-page__more-item { text-align: right; }
.visual-page__more-item:hover,
.visual-page__more-item:focus-visible {
    background: var(--surface-3);
    color: var(--text-strong);
}
.visual-page__more-item.is-active {
    background: var(--kmila-accent-soft, var(--surface-3));
    color: var(--kmila-accent, var(--text-strong));
    font-weight: var(--fw-semibold, 600);
}
.visual-page__more-item > .bi { flex-shrink: 0; }
.visual-page__more-item > span { flex: 1 1 auto; min-width: 0; }
.visual-page__more-sep {
    height: 1px;
    margin: 4px 6px;
    background: var(--border-subtle);
}

.visual-page__error {
    padding: var(--space-2) var(--space-3);
    background: var(--danger-soft, #fee2e2);
    color: var(--danger-strong, #b91c1c);
    font-size: var(--fs-sm);
}

.visual-page__workspace {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
}

/* Widget palette — vertical left rail of 6 widget chips. Replaces the
   toolbar add-buttons per 2026-06-28 UI audit Phase 6b. Width is sized to
   the chip content (~96 px) so the canvas keeps maximum horizontal real
   estate. Chips are stacked vertically; each is a button that emits an
   OnSelected event upstream to VisualPanel's OnPaletteSelected dispatcher. */
.visual-page__palette {
    flex: 0 0 116px;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-2);
    border-right: 1px solid var(--border-subtle);
    background: var(--surface-2);
    overflow-y: auto;
}

/* Collapsible category group inside the palette. Desktop: vertical list
   with a header row (icon + label + chevron) that toggles its chip body. */
.visual-page__palette-group {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.visual-page__palette-group-header {
    appearance: none;
    display: flex;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-1) var(--space-2);
    background: transparent;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    cursor: pointer;
    width: 100%;
    transition: background-color var(--duration-fast) var(--ease-standard),
                color var(--duration-fast) var(--ease-standard);
}

.visual-page__palette-group-header:hover,
.visual-page__palette-group-header:focus-visible {
    background: var(--surface-3);
    color: var(--text-default);
    outline: none;
}

.visual-page__palette-group-icon {
    display: inline-flex;
    color: var(--kmila-accent);
    font-size: calc(14px * var(--user-text-scale, 1));
    line-height: 1;
}

.visual-page__palette-group-label {
    flex: 1;
    min-width: 0;
    text-align: start;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.visual-page__palette-group-chevron {
    display: inline-flex;
    font-size: calc(12px * var(--user-text-scale, 1));
    line-height: 1;
    transition: transform var(--duration-fast) var(--ease-standard);
}

.visual-page__palette-group[data-open="false"] .visual-page__palette-group-chevron {
    transform: rotate(-90deg);
}

.visual-page__palette-group-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

.visual-page__palette-group[data-open="false"] .visual-page__palette-group-body {
    display: none;
}

.visual-page__palette-title {
    margin: 0 0 var(--space-2);
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    font-family: var(--font-mono);
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
    text-align: center;
}

.visual-page__palette-chip {
    appearance: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1);
    padding: var(--space-2) var(--space-1);
    background: var(--surface-1);
    border: 1px solid var(--border-default);
    border-radius: var(--radius-md);
    color: var(--text-default);
    font-family: inherit;
    font-size: var(--fs-xs);
    cursor: grab;
    transition: background-color var(--duration-fast) var(--ease-standard),
                border-color var(--duration-fast) var(--ease-standard),
                transform var(--duration-fast) var(--ease-standard);
}

.visual-page__palette-chip:hover:not(:disabled) {
    background: var(--surface-3);
    border-color: var(--kmila-accent);
}

.visual-page__palette-chip:active:not(:disabled) {
    cursor: grabbing;
    transform: scale(0.96);
}

.visual-page__palette-chip:focus-visible {
    outline: 2px solid var(--kmila-accent);
    outline-offset: 2px;
}

.visual-page__palette-chip:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.visual-page__palette-chip-icon {
    font-size: calc(18px * var(--user-text-scale, 1));
    color: var(--kmila-accent);
    line-height: 1;
}

.visual-page__palette-chip-label {
    font-size: var(--fs-xs);
    line-height: 1.2;
    text-align: center;
    word-break: break-word;
}

/* Mobile: stack the palette ABOVE the canvas, and the props pane BELOW it,
   so the canvas keeps the full viewport width. Without flipping the
   workspace flex direction the palette stayed as a left-side track even
   after its own direction was rotated — the canvas + empty-state ended up
   squashed into an ~80 px right column.
   (Editor-mobile audit 2026-06-29, finding V-1.) */
@media (max-width: 640.98px) {
    .visual-page__workspace {
        flex-direction: column;
    }
    .visual-page__palette {
        flex: 0 0 auto;
        flex-direction: row;
        overflow-x: auto;
        overflow-y: hidden;
        border-right: none;
        border-bottom: 1px solid var(--border-subtle);
        padding: var(--space-2);
        /* Right-edge mask hints there are more chips beyond the strip. */
        -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 24px), transparent 100%);
                mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 24px), transparent 100%);
        scrollbar-width: none;
    }
    .visual-page__palette::-webkit-scrollbar { display: none; }
    .visual-page__palette-title { display: none; }
    .visual-page__palette-chip { flex: 0 0 88px; }
    /* Mobile: each group becomes an inline pill header followed by its
       chip strip; collapsed groups render just the pill. All groups sit
       in the same horizontally-scrolling palette strip. */
    .visual-page__palette-group {
        flex-direction: row;
        align-items: center;
        gap: var(--space-1);
        flex-shrink: 0;
        padding-inline-end: var(--space-2);
        border-inline-end: 1px solid var(--border-subtle);
    }
    .visual-page__palette-group:last-child { border-inline-end: none; }
    .visual-page__palette-group-header {
        flex: 0 0 auto;
        width: auto;
        padding: var(--space-1) var(--space-2);
        background: var(--surface-1);
        border: 1px solid var(--border-default);
        border-radius: var(--radius-pill);
    }
    .visual-page__palette-group-body {
        flex-direction: row;
        gap: var(--space-1);
    }
    /* Props card on phones is narrower + max-height clipped so it doesn't
       obscure the widget. It's still absolutely positioned inside
       canvas-stack (see base rule below) — the inline style supplies
       widget-anchored left/top. */
    .visual-page__props {
        width: min(272px, calc(100vw - 2 * var(--space-3)));
        max-height: 45dvh;
        padding: var(--space-3);
        gap: var(--space-2);
    }
    /* Canvas + canvas-stack must be allowed to shrink in the column flex,
       otherwise the inner SVG min-content keeps it 600 px+ tall and the
       props pane gets pushed below the dock. */
    .visual-page__workspace > .kviz-canvas,
    .visual-page__workspace > .visual-page__canvas-stack {
        min-height: 200px;
    }
    /* V-3: hide the WebKit scrollbar track on the canvas on mobile. */
    .visual-page__workspace > .kviz-canvas,
    .visual-page__workspace > .visual-page__canvas-stack {
        scrollbar-width: none;
    }
    .visual-page__workspace > .kviz-canvas::-webkit-scrollbar,
    .visual-page__workspace > .visual-page__canvas-stack::-webkit-scrollbar {
        display: none;
    }
    /* V-2: Visual toolbar wraps awkwardly with 4+1 buttons. Switch to a
       single horizontal scrollable row so all buttons remain reachable and
       the "Saved" pill no longer orphans on row 2.
       Scroll-snap ensures the strip settles on a whole button after a swipe
       instead of mid-label — earlier mobile builds showed "Dete…", "…nect",
       "…arda" at the strip edges when the user came to rest mid-scroll. */
    .visual-page__toolbar {
        flex-wrap: nowrap;
        overflow-x: auto;
        scrollbar-width: none;
        margin-left: 0;
        width: 100%;
        scroll-snap-type: x proximity;
        scroll-padding-inline: var(--space-2);
    }
    .visual-page__toolbar::-webkit-scrollbar { display: none; }
    .visual-page__toolbar > * { flex-shrink: 0; scroll-snap-align: start; }
    /* Primary CTA (Run/Stop) sticks to the leading edge of the scrolling
       toolbar strip so it stays reachable even after the user drags the
       strip to inspect a trailing button. Without this the primary action
       can scroll fully off-screen — the biggest reachability regression on
       phones. Uses inset-inline-start so RTL flips the anchor edge.
       Toolbar-matching background + trailing shadow separate it visually
       from the scrolling buttons that pass underneath — otherwise the
       outlined primary button reads as translucent and the icons behind it
       bleed through. */
    .visual-page__toolbar .kmila-btn.kmila-btn--primary,
    .visual-page__toolbar .kmila-btn.kmila-btn--danger {
        position: sticky;
        inset-inline-start: 0;
        z-index: 2;
        background-color: var(--surface-2);
        box-shadow: var(--space-2) 0 var(--space-2) calc(-1 * var(--space-2)) var(--surface-2);
        /* Sticky positioning inside the flex row was collapsing the button
           to icon-only width. Force the natural width so both icon and label
           always render; the label is what actually communicates the action. */
        flex: 0 0 auto;
        width: auto;
        min-width: max-content;
    }

    /* Compact-label pattern on very narrow phones: every non-primary toolbar
       button that carries both an icon and a text `<span>` label collapses to
       its icon so all ten buttons fit in the visible strip without the primary
       Run/Stop CTA scrolling off-screen. Tooltip + aria-label carry the label
       for a11y. Primary + danger CTAs are excluded so Ejecutar / Detener keep
       their words — that button's colour alone isn't enough for first-time
       users. The clear-changes button has inline text (not wrapped in <span>)
       so it needs the font-size trick to hide its label. */
    .visual-page__toolbar .kmila-btn:not(.kmila-btn--primary):not(.kmila-btn--danger) > span {
        display: none;
    }
    .visual-page__toolbar [data-testid="visual-clear-changes"] {
        font-size: 0;
        gap: 0;
    }
    .visual-page__toolbar [data-testid="visual-clear-changes"] > i {
        font-size: var(--fs-base);
    }
    /* Dirty-state dot on the Save button — replaces the "Save*" text label
       once the icon-only mode kicks in. Pure CSS so the button node keeps its
       structure across breakpoints. */
    .visual-page__toolbar .kmila-btn.is-dirty {
        position: relative;
    }
    .visual-page__toolbar .kmila-btn.is-dirty::after {
        content: "";
        position: absolute;
        top: 4px;
        inset-inline-end: 4px;
        width: 6px;
        height: 6px;
        border-radius: 50%;
        background: var(--kmila-accent, currentColor);
    }
}

.visual-page__workspace > .kviz-canvas,
.visual-page__workspace > .visual-page__canvas-stack {
    flex: 1 1 auto;
    overflow: auto;
    background: var(--surface-1);
    position: relative;
}

/* Phase 3 wire overlay: the SVG sits on top of the canvas (same flex slot)
   with pointer-events: none so widget clicks pass through. The canvas-stack
   wrapper exists so the SVG can be a sibling and shares scroll behavior. */
.visual-page__canvas-stack {
    position: relative;
    overflow: hidden;   /* pan-zoom lib clips the transformed wrap here */
}

/* Pan-zoom target: wraps canvas + wires + props so they translate/scale
   together. Its own dimensions are inline-styled to canvas dims so the
   pan-zoom lib measures a stable natural size regardless of canvas
   background/grid state. */
.visual-page__canvas-wrap {
    position: relative;
    /* transform-origin + transform get set by the pan-zoom JS on attach. */
}
.visual-page__wires {
    position: absolute;
    inset: 0;
    pointer-events: none;
    overflow: visible;
}
.visual-page__wire-line {
    stroke: color-mix(in srgb, var(--accent, #f59e0b) 70%, transparent);
    stroke-width: 1.5;
    stroke-dasharray: 4 3;
    fill: none;
}
.visual-page__wire-chip {
    fill: var(--surface-3, #1f2532);
    stroke: var(--accent, #f59e0b);
    stroke-width: 1;
}
.visual-page__wire-label {
    fill: var(--text, #f8fafc);
    font-family: var(--font-mono);
    font-size: 11px;
    dominant-baseline: middle;
}

/* §6 Item 4 (2026-07-05) — locator popover. Same glass-slab language as
   props card, but pinned to canvas-stack's top-right (not widget-anchored).
   Fixed width so long widget labels don't reshape the panel; internal
   scroll for long widget lists. */
.visual-page__locator {
    position: absolute;
    top: 16px;
    right: 16px;
    z-index: 6;
    width: 260px;
    max-height: 60dvh;
    display: flex;
    flex-direction: column;
    padding: var(--space-3);
    background: color-mix(in srgb, var(--surface-2) 88%, transparent);
    -webkit-backdrop-filter: blur(14px) saturate(1.4);
    backdrop-filter: blur(14px) saturate(1.4);
    border: 1px solid color-mix(in srgb, var(--kmila-accent) 12%, var(--border-default));
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-3);
    color: var(--text);
}
.visual-page__locator-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    margin-bottom: var(--space-2);
}
.visual-page__locator-header h6 {
    margin: 0;
    font-size: 0.9rem;
    font-weight: 600;
}
.visual-page__locator-close {
    background: transparent;
    border: 0;
    color: var(--text-2);
    font-size: 1.1rem;
    line-height: 1;
    padding: 0 var(--space-1);
    cursor: pointer;
}
.visual-page__locator-list {
    list-style: none;
    margin: 0;
    padding: 0;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.visual-page__locator-item {
    display: block;
    width: 100%;
    text-align: start;
    padding: var(--space-1) var(--space-2);
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    color: var(--text);
    font-size: 0.85rem;
    cursor: pointer;
    transition: background 120ms ease, border-color 120ms ease;
}
.visual-page__locator-item:hover {
    background: color-mix(in srgb, var(--kmila-accent) 10%, transparent);
    border-color: color-mix(in srgb, var(--kmila-accent) 25%, transparent);
}
.visual-page__locator-item.is-active {
    background: color-mix(in srgb, var(--kmila-accent) 18%, transparent);
    border-color: color-mix(in srgb, var(--kmila-accent) 40%, transparent);
}
.visual-page__locator-item-label {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.visual-page__props {
    /* Floating widget-anchored card. Lives inside .visual-page__canvas-stack
       (which is position: relative) so left/top from the inline style are
       expressed in canvas coordinates — the card scrolls with the canvas
       and stays attached during drag. Glass surface + strong shadow so it
       reads as a floating popover, not a stacked panel (matches toast +
       block-editor slab language). */
    position: absolute;
    z-index: 5;
    width: 288px;
    max-height: 60dvh;
    padding: var(--space-4);
    background: color-mix(in srgb, var(--surface-2) 82%, transparent);
    -webkit-backdrop-filter: blur(14px) saturate(1.4);
    backdrop-filter: blur(14px) saturate(1.4);
    border: 1px solid color-mix(in srgb, var(--kmila-accent) 12%, var(--border-default));
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-3),
                inset 0 1px 0 color-mix(in srgb, #ffffff 40%, transparent);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    font-size: var(--fs-sm);
    /* Subtle enter animation — appears next to the widget, doesn't slide
       in from an unrelated edge. */
    animation: kmila-props-in 140ms ease-out;
}

@keyframes kmila-props-in {
    from { opacity: 0; transform: translateY(-2px) scale(0.98); }
    to   { opacity: 1; transform: translateY(0)    scale(1); }
}

/* Fallback for WebViews without backdrop-filter (older Android). */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .visual-page__props { background: var(--surface-2); }
}

.visual-page__props label {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    font-size: var(--fs-xs);
    font-weight: var(--fw-medium);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.visual-page__props label .kmila-input {
    /* The uppercase-label styling shouldn't inherit into the control. */
    text-transform: none;
    letter-spacing: 0;
    font-weight: var(--fw-regular);
    color: var(--text-default);
    font-size: var(--fs-sm);
}

.visual-page__props-header {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
    margin-bottom: var(--space-1);
}

.visual-page__props-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    color: var(--kmila-accent);
    background: color-mix(in oklab, var(--kmila-accent) 14%, transparent);
    border-radius: var(--radius-md);
    font-size: calc(15px * var(--user-text-scale, 1));
}

.visual-page__props-title {
    margin: 0;
    text-transform: capitalize;
    font-size: var(--fs-md);
    font-weight: var(--fw-semibold);
    color: var(--text-strong);
    letter-spacing: -0.005em;
    /* Truncate long titles gracefully. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

/* Direction chip in the props pane header — visually differentiates
   input widgets (drive stimulus into the design) from output widgets
   (visualize design outputs). R3-5 data-integrity gate: makes intent
   explicit so users don't attempt to bind a button to a design output. */
.visual-page__props-dir {
    display: inline-flex;
    align-items: center;
    padding: 2px 8px;
    border-radius: var(--radius-pill);
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    line-height: 1.5;
    margin-inline-start: auto;
    border: 1px solid transparent;
    flex-shrink: 0;
}

.visual-page__props-dir--in {
    background: color-mix(in oklab, var(--kmila-accent) 14%, transparent);
    color: var(--kmila-accent);
    border-color: color-mix(in oklab, var(--kmila-accent) 32%, transparent);
}

.visual-page__props-dir--out {
    background: color-mix(in oklab, var(--success, #16a34a) 14%, transparent);
    color: var(--success, #16a34a);
    border-color: color-mix(in oklab, var(--success, #16a34a) 32%, transparent);
}

.visual-page__props-delete {
    appearance: none;
    background: transparent;
    border: none;
    color: var(--text-muted);
    font-size: 0.95rem;
    line-height: 1;
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-md);
    cursor: pointer;
    flex-shrink: 0;
    transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
}

.visual-page__props-delete:hover:not(:disabled),
.visual-page__props-delete:focus-visible:not(:disabled) {
    background: var(--danger-soft);
    color: var(--danger);
}

.visual-page__props-delete:focus-visible {
    outline: none;
    box-shadow: var(--shadow-focus);
}

.visual-page__props-delete:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.visual-page__props-close {
    appearance: none;
    background: transparent;
    border: none;
    color: var(--text-muted);
    font-size: 1.25rem;
    line-height: 1;
    width: 28px;
    height: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-md);
    cursor: pointer;
    flex-shrink: 0;
    transition: background-color 120ms ease, color 120ms ease;
}

.visual-page__props-close:hover,
.visual-page__props-close:focus-visible {
    background: var(--surface-3);
    color: var(--text-default);
}
.visual-page__props-close:focus-visible {
    outline: none;
    box-shadow: var(--shadow-focus);
}

/* Section label above the bindings list — quiet uppercase eyebrow that
   separates config controls from wire-binding readout. */
.visual-page__props-section {
    font-size: var(--fs-2xs);
    font-weight: var(--fw-semibold);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin: 0;
    padding-top: var(--space-3);
    border-top: 1px solid var(--border-subtle);
}

/* Phase 3++ checkbox row in the props pane (e.g. button bounce). Flips
   the default vertical label stack into a horizontal checkbox + text.
   Also restores normal (non-eyebrow) text styling for the checkbox
   label so it reads as an option, not a section header. */
.visual-page__props label.visual-page__props-checkbox {
    flex-direction: row;
    align-items: center;
    gap: var(--space-2);
    text-transform: none;
    letter-spacing: 0;
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    color: var(--text-default);
}
.visual-page__props label.visual-page__props-checkbox input[type="checkbox"] {
    width: 16px;
    height: 16px;
    accent-color: var(--kmila-accent);
    flex-shrink: 0;
}

.visual-page__props > small {
    /* Hint copy under a checkbox (e.g. VISUAL_PROP_BOUNCE_HINT).
       Marker: sits below its associated label, subtly. */
    color: var(--text-muted);
    font-size: var(--fs-xs);
    line-height: 1.4;
    margin-top: calc(-1 * var(--space-2));
    padding-inline-start: calc(16px + var(--space-2));
}

/* Wire-binding list — one row per pin. Muted eyebrow for the section
   heading; monospace key ("out", "anode") and value (port name) with
   a subtle chevron in between so users read it as "pin → port". */
.visual-page__props-binding {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}
.visual-page__props-binding-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: 4px var(--space-2);
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--surface-1) 60%, transparent);
    font-size: var(--fs-xs);
    line-height: 1.4;
}
.visual-page__props-binding-row code {
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
    color: var(--text-default);
    background: transparent;
    padding: 0;
    flex-shrink: 0;
}
.visual-page__props-binding-row .visual-page__props-binding-arrow {
    color: var(--text-muted);
    opacity: 0.6;
    flex-shrink: 0;
}
.visual-page__props-binding-row .visual-page__props-binding-port {
    font-family: var(--font-mono);
    font-size: var(--fs-2xs);
    color: var(--kmila-accent);
    font-weight: var(--fw-medium);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
.visual-page__props-binding-row .visual-page__props-binding-port--unwired {
    color: var(--text-muted);
    font-style: italic;
    font-weight: var(--fw-regular);
}
.visual-page__props-binding-empty {
    color: var(--text-muted);
    font-style: italic;
    font-size: var(--fs-xs);
    padding: 4px 0;
}

/* Canvas */
.kviz-canvas {
    position: relative;
    /* Was `touch-action: none` — blocked native pan on phones. Canvas is
       1200×800 CSS-px fixed and always wider than a phone viewport, so the
       user needs native two-finger + one-finger pan to reach widgets past
       the initial fold. Widgets keep their own `touch-action: none` (see
       `.kviz-widget` below), so drag still works: touching a widget wins
       the gesture; touching empty canvas hands the gesture to the browser
       for a native scroll. */
    touch-action: pan-x pan-y;
    user-select: none;
    -webkit-user-select: none;
}

.kviz-canvas--grid {
    background-image:
        radial-gradient(circle, var(--border-subtle) 1px, transparent 1px);
    background-size: 24px 24px;
}

.kviz-canvas--edit { cursor: default; }
.kviz-canvas--run  { cursor: default; }

.kviz-canvas-empty {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    color: var(--text-muted);
    pointer-events: none;
    font-style: italic;
}

.kviz-widget {
    position: absolute;
    transform: translate(0, 0);
    touch-action: none;
}

.kviz-widget--selected {
    outline: 2px solid var(--accent, #6366f1);
    outline-offset: 4px;
    border-radius: 4px;
}

/* LED */
.kviz-led {
    width: 64px;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
}

.kviz-led-body {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: radial-gradient(circle at 30% 30%, var(--kviz-housing-shadow, #1f2937) 0%, var(--kviz-housing-dark, #0b0f14) 80%);
    border: 2px solid var(--kviz-housing-ring, #111);
    box-shadow: inset 0 0 6px rgba(0,0,0,0.6);
    transition: background 50ms linear, box-shadow 50ms linear;
}

.kviz-led--on .kviz-led-body {
    background: radial-gradient(circle at 30% 30%, color-mix(in srgb, var(--kviz-led-color) 80%, white 20%) 0%,
                                                   var(--kviz-led-color) 70%);
    box-shadow:
        0 0 12px var(--kviz-led-color),
        0 0 24px color-mix(in srgb, var(--kviz-led-color) 60%, transparent),
        inset 0 0 6px rgba(255,255,255,0.4);
}

.kviz-led-label {
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-muted);
}

/* Button */
.kviz-button {
    width: 80px;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    cursor: pointer;
    touch-action: none;
}

.kviz-button--edit { cursor: move; }

.kviz-button-cap {
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: linear-gradient(180deg, var(--kviz-button-cap-top, #f8fafc) 0%, var(--kviz-button-cap-bottom, #cbd5e1) 100%);
    border: 2px solid var(--kviz-button-cap-border, #475569);
    display: grid;
    place-items: center;
    color: var(--kviz-button-label, #0f172a);
    font-weight: 700;
    font-size: var(--fs-sm);
    box-shadow:
        0 3px 0 var(--kviz-button-bezel, #475569),
        0 4px 8px rgba(0,0,0,0.25);
    transition: transform 60ms ease, box-shadow 60ms ease;
}

.kviz-button--pressed .kviz-button-cap {
    transform: translateY(3px);
    box-shadow:
        0 0 0 var(--kviz-button-bezel, #475569),
        0 1px 2px rgba(0,0,0,0.3);
}

.kviz-button-port {
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-muted);
}

.kviz-unknown {
    position: absolute;
    padding: 4px 8px;
    background: var(--surface-3, #f1f5f9);
    border: 1px dashed var(--text-muted);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
}

/* Wiring modal */
.kviz-wiring-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.45);
    display: grid;
    place-items: center;
    z-index: 1080;
    opacity: 0;
    pointer-events: none;
    transition: opacity 150ms ease;
}

.kviz-wiring-backdrop--open {
    opacity: 1;
    pointer-events: auto;
}

.kviz-wiring-modal,
.examples-gallery-modal {
    /* Reset Bootstrap's `.modal { position:fixed; top:0; left:0; width:100%; height:100% }`
       so the backdrop's `place-items: center` can center us. */
    position: relative;
    top: auto;
    left: auto;
    background: var(--surface-1);
    border-radius: var(--radius-lg);
    box-shadow: 0 20px 50px rgba(0,0,0,0.25);
    width: min(620px, 92vw);
    max-height: 84vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.examples-gallery-modal { width: min(760px, 94vw); }
.examples-gallery-modal__body {
    padding: var(--space-3);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    overflow-y: auto;
}
.examples-gallery-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--space-3);
}
.examples-gallery-card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
    padding: var(--space-3);
    background: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius);
    cursor: pointer;
    text-align: start;
    transition: background-color 120ms ease, border-color 120ms ease, transform 120ms ease;
}
.examples-gallery-card:hover {
    background: var(--surface-3, color-mix(in srgb, var(--surface-2) 80%, var(--accent) 20%));
    border-color: var(--accent, #f59e0b);
    transform: translateY(-1px);
}
.examples-gallery-card__icon {
    font-size: 1.6em;
    color: var(--accent, #f59e0b);
}
.examples-gallery-card__title {
    margin: 0;
    font-size: var(--fs-md);
    font-weight: 600;
    color: var(--text);
}
.examples-gallery-card__desc {
    margin: 0;
    font-size: var(--fs-sm);
    color: var(--text-muted);
    line-height: 1.4;
}
.examples-gallery-modal .modal-header,
.examples-gallery-modal .modal-footer { padding: var(--space-3); border-bottom: 1px solid var(--border-subtle); }
.examples-gallery-modal .modal-footer { border-bottom: 0; }

.kviz-wiring-modal .modal-header,
.kviz-wiring-modal .modal-footer {
    padding: var(--space-3);
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid var(--border-subtle);
}

.kviz-wiring-modal .modal-footer {
    border-bottom: 0;
    border-top: 1px solid var(--border-subtle);
}

.kviz-wiring-modal .modal-body {
    padding: var(--space-3);
    overflow-y: auto;
}

.kviz-wiring-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--fs-sm);
}

.kviz-wiring-table th,
.kviz-wiring-table td {
    padding: var(--space-2);
    border-bottom: 1px solid var(--border-subtle);
    text-align: left;
    vertical-align: middle;
}

.kviz-wiring-table th {
    color: var(--text-muted);
    font-weight: 600;
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.kviz-wiring-table select.kmila-input {
    width: 100%;
}

/* ── Switch widget (Phase 1) ─────────────────────────────────────────── */
.kviz-switch {
    width: 80px;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    cursor: pointer;
    touch-action: none;
    user-select: none;
}

.kviz-switch--edit { cursor: move; }

.kviz-switch-body {
    width: 56px;
    height: 30px;
    display: grid;
    place-items: center;
    padding: 4px;
}

.kviz-switch-track {
    width: 100%;
    height: 100%;
    border-radius: 999px;
    background: var(--kviz-switch-track-off, #cbd5e1);
    border: 1px solid var(--kviz-switch-track-off-border, #94a3b8);
    position: relative;
    transition: background 120ms ease, border-color 120ms ease;
    box-shadow: inset 0 1px 2px rgba(0,0,0,0.15);
}

.kviz-switch-knob {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: linear-gradient(180deg, var(--kviz-switch-knob-light, #f8fafc) 0%, var(--kviz-switch-knob-dark, #e2e8f0) 100%);
    border: 1px solid var(--kviz-switch-knob-border, #475569);
    position: absolute;
    top: 50%;
    left: 1px;
    transform: translateY(-50%);
    transition: left 140ms cubic-bezier(.4,.0,.2,1);
    box-shadow: 0 1px 2px rgba(0,0,0,0.35);
}

.kviz-switch--on .kviz-switch-track {
    background: var(--kviz-switch-track-on, #22c55e);
    border-color: var(--kviz-switch-track-on-border, #166534);
}

.kviz-switch--on .kviz-switch-knob {
    /* Track inner-width 48 minus knob 22 minus 1px gap each side = 24. */
    left: calc(100% - 24px);
}

.kviz-switch-label {
    font-size: var(--fs-sm);
    font-weight: 600;
}

.kviz-switch-port {
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-muted);
}

/* ── Multi-switch widget (FW-2) ──────────────────────────────────────── */
/* Horizontal DIP row. Each cell is a mini vertical switch + label so the
   widget reads like a real SPST bank on a breakout board. Levers stack
   vertically inside cells to keep the row compact at count=8. */
.kviz-multi-switch {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    touch-action: none;
    user-select: none;
}

.kviz-multi-switch--edit { cursor: move; }

.kviz-multi-switch__row {
    display: flex;
    flex-direction: row;
    gap: 4px;
    padding: 6px 8px;
    background: var(--kviz-multi-switch-body, #f1f5f9);
    border: 1px solid var(--kviz-multi-switch-body-border, #94a3b8);
    border-radius: 6px;
    box-shadow: inset 0 1px 2px rgba(0,0,0,0.12);
}

.kviz-multi-switch__cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    width: 28px;
}

.kviz-multi-switch__lever {
    width: 20px;
    height: 32px;
    border-radius: 4px;
    background: var(--kviz-switch-track-off, #cbd5e1);
    border: 1px solid var(--kviz-switch-track-off-border, #94a3b8);
    position: relative;
    transition: background 120ms ease, border-color 120ms ease;
    box-shadow: inset 0 1px 2px rgba(0,0,0,0.15);
    cursor: pointer;
}

.kviz-multi-switch__knob {
    width: 16px;
    height: 12px;
    border-radius: 2px;
    background: linear-gradient(180deg, var(--kviz-switch-knob-light, #f8fafc) 0%, var(--kviz-switch-knob-dark, #e2e8f0) 100%);
    border: 1px solid var(--kviz-switch-knob-border, #475569);
    position: absolute;
    left: 50%;
    top: calc(100% - 14px);
    transform: translateX(-50%);
    transition: top 140ms cubic-bezier(.4,.0,.2,1);
    box-shadow: 0 1px 2px rgba(0,0,0,0.35);
}

.kviz-multi-switch__lever--on {
    background: var(--kviz-switch-track-on, #22c55e);
    border-color: var(--kviz-switch-track-on-border, #166534);
}

.kviz-multi-switch__lever--on .kviz-multi-switch__knob {
    top: 2px;
}

.kviz-multi-switch__cell-label {
    font-size: var(--fs-xs);
    font-weight: 600;
    color: var(--text-muted);
    white-space: nowrap;
    max-width: 32px;
    overflow: hidden;
    text-overflow: ellipsis;
}

.kviz-multi-switch__port {
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-muted);
}

/* ── LED array widget (Phase 1) ──────────────────────────────────────── */
.kviz-led-array {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    touch-action: none;
    user-select: none;
}

.kviz-led-array__strip {
    display: flex;
    gap: 4px;
    padding: 6px;
    background: var(--kviz-housing-dark, #0b0f14);
    border-radius: 8px;
    border: 1px solid var(--kviz-housing-border, #1f2937);
    box-shadow: inset 0 1px 3px rgba(0,0,0,0.6);
}

.kviz-led-array--v .kviz-led-array__strip {
    flex-direction: column;
}

.kviz-led-array__cell {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: radial-gradient(circle at 30% 30%, var(--kviz-housing-shadow, #1f2937) 0%, var(--kviz-housing-dark, #0b0f14) 80%);
    border: 1px solid var(--kviz-housing-ring, #111);
    transition: background 50ms linear, box-shadow 50ms linear;
}

.kviz-led-array__cell--on {
    background: radial-gradient(circle at 30% 30%,
        color-mix(in srgb, var(--kviz-led-color) 80%, white 20%) 0%,
        var(--kviz-led-color) 70%);
    box-shadow:
        0 0 6px var(--kviz-led-color),
        0 0 12px color-mix(in srgb, var(--kviz-led-color) 60%, transparent),
        inset 0 0 3px rgba(255,255,255,0.5);
}

.kviz-led-array__port {
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-muted);
}

/* ───── FW-1: 3×3 mono LED matrix ─────
   Fixed 3×3 grid of small round LEDs. Cell size + gap tuned so the housing
   comes out at ~96 CSS-px — matches the phone-safe footprint declared in
   VisualPanel.AddLedMatrix. --kviz-matrix-color is set inline by the widget
   from the color config; --kviz-matrix-on-opacity carries the brightness
   knob (0..1). */
.kviz-matrix-3x3 {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    touch-action: none;
    user-select: none;
}

.kviz-matrix-3x3--edit { cursor: move; }

.kviz-matrix-3x3__grid {
    display: grid;
    grid-template-columns: repeat(3, 20px);
    grid-template-rows: repeat(3, 20px);
    gap: 6px;
    padding: 8px;
    background: var(--kviz-housing-dark, #0b0f14);
    border-radius: 8px;
    border: 1px solid var(--kviz-housing-border, #1f2937);
    box-shadow: inset 0 1px 3px rgba(0,0,0,0.6);
}

.kviz-matrix-3x3__cell {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: radial-gradient(circle at 30% 30%, var(--kviz-housing-shadow, #1f2937) 0%, var(--kviz-housing-dark, #0b0f14) 80%);
    border: 1px solid var(--kviz-housing-ring, #111);
    transition: background 50ms linear, box-shadow 50ms linear, opacity 50ms linear;
}

.kviz-matrix-3x3__cell--on {
    background: radial-gradient(circle at 30% 30%,
        color-mix(in srgb, var(--kviz-matrix-color) 80%, white 20%) 0%,
        var(--kviz-matrix-color) 70%);
    box-shadow:
        0 0 6px var(--kviz-matrix-color),
        0 0 12px color-mix(in srgb, var(--kviz-matrix-color) 60%, transparent),
        inset 0 0 3px rgba(255,255,255,0.5);
    opacity: var(--kviz-matrix-on-opacity, 1);
}

.kviz-matrix-3x3__port {
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-muted);
}

/* ───── FW-1b: 3×3 RGB LED matrix ─────
   Same grid layout as FW-1 mono. Cell colour comes from an inline
   --cell-color set by the widget (mix of the three R/G/B planes). Off
   cells fall back to the shared dark grey housing tone. */
.kviz-matrix-3x3-rgb {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    touch-action: none;
    user-select: none;
}

.kviz-matrix-3x3-rgb--edit { cursor: move; }

.kviz-matrix-3x3-rgb__grid {
    display: grid;
    grid-template-columns: repeat(3, 20px);
    grid-template-rows: repeat(3, 20px);
    gap: 6px;
    padding: 8px;
    background: var(--kviz-housing-dark, #0b0f14);
    border-radius: 8px;
    border: 1px solid var(--kviz-housing-border, #1f2937);
    box-shadow: inset 0 1px 3px rgba(0,0,0,0.6);
}

.kviz-matrix-3x3-rgb__cell {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: radial-gradient(circle at 30% 30%, var(--kviz-housing-shadow, #1f2937) 0%, var(--kviz-housing-dark, #0b0f14) 80%);
    border: 1px solid var(--kviz-housing-ring, #111);
    transition: background 50ms linear, box-shadow 50ms linear, opacity 50ms linear;
}

.kviz-matrix-3x3-rgb__cell--on {
    background: radial-gradient(circle at 30% 30%,
        color-mix(in srgb, var(--cell-color, #fff) 80%, white 20%) 0%,
        var(--cell-color, #fff) 70%);
    box-shadow:
        0 0 6px var(--cell-color, #fff),
        0 0 12px color-mix(in srgb, var(--cell-color, #fff) 60%, transparent),
        inset 0 0 3px rgba(255,255,255,0.5);
    opacity: var(--kviz-matrix-on-opacity, 1);
}

.kviz-matrix-3x3-rgb__port {
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-muted);
}

/* ───── FW-5: Piezo buzzer ─────
   Speaker icon inside a rounded chip. Two concentric pulse rings animate
   when the buzzer is active (either enable = '1' in freq mode, or
   signal = '1' in raw mode). Rings and glow use the shared brand accent
   so the buzzer visually reads as an "output" alongside LED-family
   widgets. */
.kviz-buzzer {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 4px;
    touch-action: none;
    user-select: none;
}

.kviz-buzzer--edit { cursor: move; }

.kviz-buzzer__body {
    position: relative;
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--kmila-brand-slate, #3b82f6);
    background: var(--kviz-housing-dark, #0b0f14);
    border-radius: 50%;
    border: 1px solid var(--kviz-housing-border, #1f2937);
}

.kviz-buzzer__icon {
    width: 22px;
    height: 22px;
    z-index: 1;
}

.kviz-buzzer__ring {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 40px;
    height: 40px;
    margin: -20px 0 0 -20px;
    border-radius: 50%;
    border: 2px solid var(--kmila-brand-slate, #3b82f6);
    opacity: 0;
    pointer-events: none;
}

.kviz-buzzer--active .kviz-buzzer__ring--1 {
    animation: kviz-buzzer-pulse 900ms ease-out infinite;
}
.kviz-buzzer--active .kviz-buzzer__ring--2 {
    animation: kviz-buzzer-pulse 900ms ease-out infinite;
    animation-delay: 450ms;
}
.kviz-buzzer--active .kviz-buzzer__body {
    box-shadow:
        0 0 6px var(--kmila-brand-slate, #3b82f6),
        0 0 12px color-mix(in srgb, var(--kmila-brand-slate, #3b82f6) 40%, transparent);
}

@keyframes kviz-buzzer-pulse {
    0%   { transform: scale(0.8); opacity: 0.7; }
    100% { transform: scale(2.0); opacity: 0.0; }
}

.kviz-buzzer__meta {
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-family: var(--font-mono);
}

.kviz-buzzer__freq {
    font-size: var(--fs-xs);
    color: var(--text-color, #111);
    font-weight: 600;
    min-width: 60px;
}

.kviz-buzzer__port {
    font-size: var(--fs-xs);
    color: var(--text-muted);
}

/* ───── FW-4: matrix keypad ─────
   3×3 / 3×4 / 4×3 / 4×4 grid of pill-shaped keys. Each key is a 42×42
   CSS-px square with a rounded background; `--pressed` swaps in a
   filled accent for a tactile press feedback. Cols count is set on
   the grid template inline via `grid-template-columns` per widget. */
.kviz-keypad {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    touch-action: none;
    user-select: none;
}

.kviz-keypad--edit { cursor: move; }

.kviz-keypad__grid {
    display: grid;
    /* grid-template-columns supplied inline (repeat(Cols, 42px)) */
    grid-auto-rows: 42px;
    gap: 4px;
    padding: 6px;
    background: var(--surface-card, #ffffff);
    border-radius: 10px;
    border: 1px solid var(--border-color, #d0d5dd);
    box-shadow: inset 0 1px 3px rgba(0,0,0,0.08);
}

.kviz-keypad__key {
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: calc(14px * var(--user-text-scale, 1));
    background: var(--surface-input, #f8fafc);
    color: var(--text-color, #111);
    border-radius: 8px;
    border: 1px solid var(--border-color, #d0d5dd);
    box-shadow: 0 1px 0 rgba(0,0,0,0.05);
    transition: background 40ms linear, transform 40ms linear;
    cursor: pointer;
}

.kviz-keypad__key--pressed {
    background: var(--kmila-brand-slate, #3b82f6);
    color: #ffffff;
    transform: translateY(1px);
    box-shadow: inset 0 1px 3px rgba(0,0,0,0.25);
}

.kviz-keypad__port {
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-muted);
}

/* ───── FW-3: HD44780 character LCD ─────
   rows×cols cell grid rendering each ASCII char in a monospace green-
   on-dark cell to evoke a real backlit LCD. Backlight config toggles
   the `--on` / `--off` glow. Cell width fixed at 14 CSS-px so 16×2
   displays at ~240 px and 20×4 at ~300 px — fits mobile viewports. */
.kviz-lcd {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    touch-action: none;
    user-select: none;
}

.kviz-lcd--edit { cursor: move; }

.kviz-lcd__screen {
    display: grid;
    /* grid-template-columns supplied inline (repeat(Cols, 14px)) */
    grid-auto-rows: 18px;
    gap: 1px;
    padding: 6px 8px;
    background: #163f2c;
    border-radius: 4px;
    border: 2px solid #0b2a1c;
    box-shadow: inset 0 1px 2px rgba(0,0,0,0.6);
    color: #a7f3c4;
    font-family: var(--font-mono);
    font-weight: 600;
    font-size: calc(14px * var(--user-text-scale, 1));
    line-height: 1;
}

.kviz-lcd--on .kviz-lcd__screen {
    background: #14532d;
    color: #ecfccb;
    box-shadow:
        inset 0 1px 2px rgba(0,0,0,0.4),
        0 0 6px rgba(163, 230, 53, 0.4);
}

.kviz-lcd--off .kviz-lcd__screen {
    background: #1a2e24;
    color: #4b6e58;
}

.kviz-lcd__cell {
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    overflow: hidden;
}

.kviz-lcd__char { white-space: pre; }

.kviz-lcd__port {
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-muted);
}

/* ───── Phase 3: 7-segment display ─────
   Standard 7-segment layout: 6 outer bars (a-f) form an "8", g is the
   middle horizontal bar, dp is a small dot at the lower-right. Each
   segment uses CSS polygon clip-path so the corners notch correctly. */
.kviz-7seg {
    width: 80px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    user-select: none;
    -webkit-user-select: none;
}
.kviz-7seg--edit { cursor: move; }
.kviz-7seg__housing {
    position: relative;
    width: 60px;
    height: 100px;
    background: var(--surface-3, #1f2532);
    border: 2px solid var(--border, #3a4053);
    border-radius: 6px;
}
.kviz-7seg__seg {
    position: absolute;
    background: color-mix(in srgb, var(--kviz-7seg-color, #ef4444) 12%, transparent);
    transition: background 80ms linear, box-shadow 80ms linear;
}
.kviz-7seg__seg--on {
    background: var(--kviz-7seg-color, #ef4444);
    box-shadow:
        0 0 4px var(--kviz-7seg-color, #ef4444),
        0 0 8px color-mix(in srgb, var(--kviz-7seg-color, #ef4444) 60%, transparent);
}
/* Horizontal segments: 32×6 px bars with chamfered ends. */
.kviz-7seg__seg--a,
.kviz-7seg__seg--d,
.kviz-7seg__seg--g {
    left: 14px;
    width: 32px;
    height: 6px;
    clip-path: polygon(3px 0, 29px 0, 32px 3px, 29px 6px, 3px 6px, 0 3px);
}
.kviz-7seg__seg--a { top: 6px; }
.kviz-7seg__seg--g { top: 47px; }
.kviz-7seg__seg--d { top: 88px; }
/* Vertical segments: 6×38 px bars (split into two ~18-px halves by g). */
.kviz-7seg__seg--b,
.kviz-7seg__seg--c,
.kviz-7seg__seg--e,
.kviz-7seg__seg--f {
    width: 6px;
    height: 38px;
    clip-path: polygon(0 3px, 3px 0, 6px 3px, 6px 35px, 3px 38px, 0 35px);
}
.kviz-7seg__seg--f { left: 8px;  top: 8px; }
.kviz-7seg__seg--b { left: 46px; top: 8px; }
.kviz-7seg__seg--e { left: 8px;  top: 52px; }
.kviz-7seg__seg--c { left: 46px; top: 52px; }
/* Decimal point: a 6×6 dot at the lower-right corner. */
.kviz-7seg__seg--dp {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    left: 50px;
    top: 88px;
    clip-path: none;
}
.kviz-7seg__readout {
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* ───── Ring 5 (2026-07-14): 7-seg bank (multi-digit composite) ─────
   Tiles N SevenSegmentWidget-style digits side by side. Reuses the
   .kviz-7seg__housing + .kviz-7seg__seg selectors so segment art stays
   in sync with the single-digit widget. */
.kviz-7seg-bank {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    user-select: none;
    -webkit-user-select: none;
}
.kviz-7seg-bank--edit { cursor: move; }
.kviz-7seg-bank__row {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 6px;
}
.kviz-7seg-bank__port {
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    padding-left: 2px;
}

/* ───── Phase 2: Stepper widget ───── */
.kviz-stepper {
    width: 130px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    user-select: none;
    -webkit-user-select: none;
}
.kviz-stepper--edit { cursor: move; }
.kviz-stepper-housing {
    position: relative;
    width: 110px;
    height: 110px;
    border-radius: 50%;
    background:
        radial-gradient(circle at 30% 30%, color-mix(in srgb, var(--surface-3, #1f2532) 70%, #fff 5%), var(--surface-3, #1f2532) 70%);
    border: 2px solid var(--border, #3a4053);
    box-shadow: inset 0 0 12px rgba(0,0,0,0.45);
    overflow: hidden;
}
/* Rotor disk pinned at center, transform: rotate(angle) makes it spin. */
.kviz-stepper-rotor {
    position: absolute;
    inset: 14px;
    border-radius: 50%;
    background: linear-gradient(135deg, var(--kviz-stepper-rotor-light, #9ca3af), var(--kviz-stepper-rotor-dark, #4b5563));
    border: 1px solid rgba(0,0,0,0.4);
    transition: transform 80ms linear;
    transform-origin: 50% 50%;
}
/* The "north" arm — a small notch on the rotor so the rotation is visible. */
.kviz-stepper-rotor__arm {
    position: absolute;
    left: 50%;
    top: 4px;
    width: 8px;
    height: 22px;
    background: linear-gradient(180deg, var(--kviz-stepper-coil-light, #fde68a), var(--kviz-stepper-coil-dark, #f59e0b));
    border-radius: 2px;
    transform: translateX(-50%);
    box-shadow: 0 0 6px rgba(250,204,21,0.6);
}
/* Four coil pads around the housing. Compass-style positions. */
.kviz-stepper-coil {
    position: absolute;
    width: 18px;
    height: 18px;
    border-radius: 4px;
    font-size: calc(10px * var(--user-text-scale, 1));
    font-family: var(--font-mono);
    display: flex; align-items: center; justify-content: center;
    background: var(--surface-2, #2a3142);
    color: var(--text-muted);
    border: 1px solid var(--border, #3a4053);
    transition: background-color 120ms ease, color 120ms ease, box-shadow 120ms ease;
}
.kviz-stepper-coil--n { top: -2px;    left: calc(50% - 9px); }
.kviz-stepper-coil--e { right: -2px;  top:  calc(50% - 9px); }
.kviz-stepper-coil--s { bottom: -2px; left: calc(50% - 9px); }
.kviz-stepper-coil--w { left: -2px;   top:  calc(50% - 9px); }
.kviz-stepper-coil--lit {
    background: var(--accent, #f59e0b);
    color: #1a1a1a;
    box-shadow: 0 0 8px color-mix(in srgb, var(--accent, #f59e0b) 70%, transparent);
}
.kviz-stepper-meta {
    display: flex;
    gap: 8px;
    font-size: var(--fs-xs);
    font-family: var(--font-mono);
    color: var(--text-muted);
}
.kviz-stepper-angle { color: var(--text); }
.kviz-stepper-steps {
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* ───── Phase 2: run controls (speed slider, scrub, breakpoints) ───── */
.visual-page__runbar {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 8px 12px;
    background: var(--surface-2, #2a3142);
    border-bottom: 1px solid var(--border, #3a4053);
}
.visual-page__runbar-row {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
}
.visual-page__runbar-row--disabled { opacity: 0.55; }
.visual-page__runbar-cell {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: var(--fs-xs);
}
.visual-page__runbar-cell--grow { flex: 1 1 240px; }
.visual-page__runbar-cell > small {
    color: var(--text-muted);
    font-size: var(--fs-2xs, 0.7rem);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.visual-page__speed-slider { width: 120px; }
.visual-page__speed-label {
    font-family: var(--font-mono);
    color: var(--text);
    min-width: 3em;
}
.visual-page__delay-input { width: 6em; }
.visual-page__pause-btn { min-width: 7em; }
.visual-page__tick-readout {
    margin-left: auto;
    font-family: var(--font-mono);
    color: var(--text-muted);
    font-size: var(--fs-xs);
}
.visual-page__bp-hit {
    color: var(--accent, #f59e0b);
    font-weight: 600;
}
.visual-page__scrub-slider {
    flex: 1 1 auto;
    width: 100%;
    accent-color: var(--accent, #f59e0b);
}
.visual-page__scrub-readout {
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    color: var(--text-muted);
    min-width: 12em;
}
.visual-page__bp-input { width: 7em; }
.visual-page__bp-list {
    display: flex;
    gap: 4px;
    flex-wrap: wrap;
}
.visual-page__bp-chip {
    border: 1px solid var(--border, #3a4053);
    background: var(--surface-3, #1f2532);
    color: var(--accent, #f59e0b);
    border-radius: 14px;
    padding: 2px 10px;
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    cursor: pointer;
    transition: background-color 120ms ease;
}
.visual-page__bp-chip:hover {
    background: var(--surface-4, #15192a);
}

/* ============================================================================
   KmilaTooltip — universal styled tooltip (v=30, 2026-07-19).

   ARCHITECTURE (rewritten in v=30):
     • Wrapper span is `display: block` so it doesn't shrink full-width
       children (widget palette chips previously collapsed to content width
       under the v=29 `display: inline-flex`). In flex-row toolbars the
       wrapper still sizes to content because flex items with `width: auto`
       are content-based; in flex-column parents (`align-items: stretch`
       default) it stretches to fill the cross-axis, which is what palette
       chips want. Block wrapper works in both cases where inline-flex
       failed on the second.
     • Bubble (`::after`) and arrow (`::before`) are `position: fixed`. That
       escapes BOTH ancestor stacking contexts AND ancestor overflow:hidden
       / overflow:auto clipping — the two failure modes that made v=29
       tooltips render "behind" the Visual editor's canvas card (canvas
       owned a competing stacking context; toolbar had overflow-x:auto that
       implicitly clipped y too).
     • Position is driven by CSS custom properties (--kt-l/--kt-t/--kt-r/
       --kt-b/--kt-w/--kt-h) which kmila-shared-head.js refreshes on every
       mouseover + focusin against the wrapper's getBoundingClientRect().
       No Blazor JS interop needed — a single delegated listener pair on
       document handles every wrapper on the page.
     • z-index of 2147483000 sits just below int32 max, guaranteeing the
       bubble wins against every modal / dialog / overlay in the app.
   ============================================================================ */
.kmila-tooltip {
    /* Wrapper is truly layout-neutral via display: contents — the wrapper
       generates no box, so its child (typically a <button>) sits in the
       parent flex/grid/block layout exactly as it did before wrapping.
       This is what fixes "widget palette chips don't take full column
       width": with display: block the chip was a grandchild of the flex
       column and <button> quirks kept it content-sized; with contents the
       chip is once again a DIRECT flex item, inheriting align-items:
       stretch cleanly. The ::after bubble still works because it uses
       position: fixed anchored to CSS variables (below) — it doesn't need
       the wrapper to be a positioning context. */
    display: contents;
}

.kmila-tooltip__sr {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.kmila-tooltip::after,
.kmila-tooltip::before {
    position: fixed;
    z-index: 2147483000;   /* Guaranteed-win layer. */
    pointer-events: none;
    opacity: 0;
    transition: opacity 120ms ease;
    transition-delay: 0ms;
    /* Fallback when kmila-shared-head.js hasn't refreshed --kt-* yet: park
       the bubble at (0,0) but keep it invisible via opacity:0. First hover
       fires the JS listener before the reveal delay finishes. */
    left: 0;
    top: 0;
}

.kmila-tooltip::after {
    content: attr(data-kmila-tip);
    padding: var(--space-1, 4px) var(--space-3, 10px);
    background-color: var(--surface-inverse, #0f172a);
    color: var(--text-inverse, #f8fafc);
    font-size: var(--fs-xs, 12px);
    font-weight: var(--fw-medium, 500);
    line-height: 1.35;
    max-width: 260px;
    width: max-content;   /* size to content, capped at max-width */
    white-space: normal;
    text-align: center;
    border-radius: var(--radius-sm, 6px);
    box-shadow: var(--shadow-2, 0 4px 12px rgba(0,0,0,0.18));
}

.kmila-tooltip::before {
    content: "";
    width: 0;
    height: 0;
    border: 5px solid transparent;
}

/* Reveal on hover + keyboard focus. `:has(:hover)` and `:has(:focus-within)`
   are required because the wrapper is display: contents — it has no box, so
   `.kmila-tooltip:hover` can never match. `:has()` reads descendant state
   regardless of the wrapper's own box, so the bubble reveals when any
   descendant (the wrapped button, typically) is hovered / focused. Support:
   Chrome 105+, Firefox 121+, Safari 15.4+ — well ahead of Kmila's target
   dates (all Kmila platforms ship 2023+ WebViews). */
.kmila-tooltip:has(:hover)::after,
.kmila-tooltip:has(:hover)::before,
.kmila-tooltip:has(:focus-within)::after,
.kmila-tooltip:has(:focus-within)::before {
    opacity: 1;
    transition-delay: 500ms;
}

/* --- Placement — TOP ---------------------------------------------------- */
.kmila-tooltip--top::after {
    left: calc(var(--kt-l, 0px) + var(--kt-w, 0px) / 2);
    top: var(--kt-t, 0px);
    transform: translate(-50%, calc(-100% - 8px));
}
.kmila-tooltip--top::before {
    left: calc(var(--kt-l, 0px) + var(--kt-w, 0px) / 2);
    top: var(--kt-t, 0px);
    transform: translate(-50%, calc(-100% - 3px));
    border-top-color: var(--surface-inverse, #0f172a);
    border-bottom-width: 0;
}

/* --- Placement — BOTTOM ------------------------------------------------- */
.kmila-tooltip--bottom::after {
    left: calc(var(--kt-l, 0px) + var(--kt-w, 0px) / 2);
    top: var(--kt-b, 0px);
    transform: translate(-50%, 8px);
}
.kmila-tooltip--bottom::before {
    left: calc(var(--kt-l, 0px) + var(--kt-w, 0px) / 2);
    top: var(--kt-b, 0px);
    transform: translate(-50%, 3px);
    border-bottom-color: var(--surface-inverse, #0f172a);
    border-top-width: 0;
}

/* --- Placement — LEFT (bubble on the wrapper's LTR-left) ---------------- */
.kmila-tooltip--left::after {
    left: var(--kt-l, 0px);
    top: calc(var(--kt-t, 0px) + var(--kt-h, 0px) / 2);
    transform: translate(calc(-100% - 8px), -50%);
}
.kmila-tooltip--left::before {
    left: var(--kt-l, 0px);
    top: calc(var(--kt-t, 0px) + var(--kt-h, 0px) / 2);
    transform: translate(calc(-100% - 3px), -50%);
    border-left-color: var(--surface-inverse, #0f172a);
    border-right-width: 0;
}

/* --- Placement — RIGHT (bubble on the wrapper's LTR-right) -------------- */
.kmila-tooltip--right::after {
    left: var(--kt-r, 0px);
    top: calc(var(--kt-t, 0px) + var(--kt-h, 0px) / 2);
    transform: translate(8px, -50%);
}
.kmila-tooltip--right::before {
    left: var(--kt-r, 0px);
    top: calc(var(--kt-t, 0px) + var(--kt-h, 0px) / 2);
    transform: translate(3px, -50%);
    border-right-color: var(--surface-inverse, #0f172a);
    border-left-width: 0;
}

/* RTL: Left ↔ Right placements swap so the bubble stays on the reading-
   direction side. The arrow ::before also has to flip its colored / zeroed
   border sides so the wedge still points at the button. Same convention as
   NavMenu's collapsed-sidebar tooltip. */
[dir="rtl"] .kmila-tooltip--left::after {
    left: var(--kt-r, 0px);
    transform: translate(8px, -50%);
}
[dir="rtl"] .kmila-tooltip--left::before {
    left: var(--kt-r, 0px);
    transform: translate(3px, -50%);
    border-left-color: transparent;
    border-right-color: var(--surface-inverse, #0f172a);
    border-right-width: 5px;   /* restore — LTR --left::before zeroed this */
    border-left-width: 0;      /* sharpen the wedge tip */
}
[dir="rtl"] .kmila-tooltip--right::after {
    left: var(--kt-l, 0px);
    transform: translate(calc(-100% - 8px), -50%);
}
[dir="rtl"] .kmila-tooltip--right::before {
    left: var(--kt-l, 0px);
    transform: translate(calc(-100% - 3px), -50%);
    border-right-color: transparent;
    border-left-color: var(--surface-inverse, #0f172a);
    border-left-width: 5px;    /* restore — LTR --right::before zeroed this */
    border-right-width: 0;     /* sharpen the wedge tip */
}

/* Reduced-motion: skip the fade tail. */
@media (prefers-reduced-motion: reduce) {
    .kmila-tooltip::after,
    .kmila-tooltip::before {
        transition: opacity 60ms ease;
    }
}

/* Coarse-pointer (touch) — long-press should still surface the bubble
   quickly. Shortens the 500 ms delay to 150 ms so tap-and-hold reveals. */
@media (hover: none) {
    .kmila-tooltip:hover::after,
    .kmila-tooltip:hover::before,
    .kmila-tooltip:focus-within::after,
    .kmila-tooltip:focus-within::before {
        transition-delay: 150ms;
    }
}


/* ═══════════════════════════════════════════════════════════════════════════
   Z11 · Landscape sweep (2026-07-26)
   ═══════════════════════════════════════════════════════════════════════════
   Phone-in-landscape viewports (typically ≤ 500 px tall × 500-950 px wide)
   were never gated on viewport height — every mobile shim in this stylesheet
   is width-only. The result on a 667×375 or 900×400 device is vertical
   starvation: TopBar (3.5rem) + view-tabs (~48 px) + toolbar (~44 px) +
   status bar (~24 px) ≈ 200 px of chrome, leaving 175–200 px for the actual
   editor / viewer / waveform content.

   All rules below sit behind `(orientation: landscape) and (max-height:
   500px)` so desktop, portrait mobile, and tablets in landscape are
   untouched. Rules that also want to fire on tall-but-short desktop windows
   (e.g. a laptop with a maximised browser at 1920×420) can drop the
   `orientation: landscape` clause — kept in for the phone-first sweep.

   Fix layers reclaim ~40–60 px of vertical: TopBar padding tightens,
   view-tabs lose top padding, waveform toolbar collapses to a compact
   min-height, and the schematic info bottom-sheet climbs from 50 dvh to
   65 dvh so text is legible at 375-tall. Monaco keeps its existing 240 px
   floor from the earlier `(max-width: 768) and (orientation: landscape)`
   block at :2447. */

@media (orientation: landscape) and (max-height: 500px) {
    /* TopBar — reclaim ~16 px */
    .kmila-topbar,
    .kmila-topbar__inner {
        min-height: 2.5rem;
    }
    .kmila-topbar__inner {
        padding-block: var(--space-1);
    }

    /* Editor view-tabs — reclaim ~8 px */
    .editor-workbench__view-tabs {
        padding-top: var(--space-1);
    }

    /* Waveform toolbar — pinned sticky, don't let it dominate a 375-tall
       viewport. `min-height: 44px` was set for tap-target purposes; keep
       the same floor but shrink vertical padding so the inner buttons
       don't stack the toolbar into a 60+ px band. */
    .waveform-viewer__toolbar {
        padding-block: var(--space-1);
    }

    /* Schematic info bottom-sheet — the base rule at :10161 caps it at
       50 dvh. On a 375-tall viewport that's 187 px, too small for the
       inspector's typical 4-6 lines of key/value data. Lift to 65 dvh
       (≈ 244 px) which is still short enough to leave the canvas visible
       above the sheet. */
    .schematic-info {
        max-height: 65dvh;
    }
    .schematic-basket {
        /* Basket sits above the info sheet — move it in lock-step so the
           two don't overlap when the sheet grows. */
        bottom: 66dvh;
    }

    /* Schematic + Flow canvas bodies — no aspect-ratio, no min-height. In
       landscape short viewports the surrounding flex chrome (toolbar +
       info panel + basket) could squeeze the canvas below usability. Pin
       a floor so the drag surface stays large enough to render meaningful
       geometry. 40 dvh at 375-tall = 150 px which is small but scrollable
       via pan-zoom; below that gestures become unreliable. */
    .schematic-viewer__body,
    .flow-diagram__body {
        min-height: 40dvh;
    }
}


/* ═══════════════════════════════════════════════════════════════════════════
   Z12 · Soft-keyboard sweep (2026-07-26)
   ═══════════════════════════════════════════════════════════════════════════
   Neither `100vh` nor `100dvh` shrinks when a soft keyboard opens — both are
   keyboard-INCLUSIVE flavors of the layout viewport. So a modal capped at
   `max-height: 85dvh` on a 375-tall phone with a 200-px keyboard visible
   only exposes ~175 px of the modal; the footer buttons (submit, save, etc.)
   render off-screen, invisible under the IME.

   `kmila-shared-head.js` section Z12 publishes two hooks onto <html> that
   this block reads:
     • `--kmila-viewport-h`  → visualViewport.height in px (kbd-aware)
     • `data-kmila-kbd="open"` → set when keyboard consumes ≥ 150 px

   Any rule that would trap a control under the keyboard should either:
     a) Cap its `max-height` at `min(<base>, var(--kmila-viewport-h) - <chrome>)`
        so it never grows past the visible area, OR
     b) Hide itself while the keyboard is open, since it competes for the
        exact space the user is trying to type into.

   The choice below leans on (a) for MODAL content (needs to stay reachable)
   and (b) for BOTTOM-STICKY UI like the phone dock / run-bar (never useful
   while the user is typing into an input inside a modal or the editor). */

/* Every published control that caps at `dvh` — the caps are what trap
   controls under the keyboard, so shrink them into the visible band. The
   `min(...)` keeps the desktop / kbd-closed cap intact via fallback. */
.modal-content {
    max-height: min(85dvh, var(--kmila-viewport-h, 85dvh) - 16px);
}
.file-row__menu-panel {
    max-height: min(80vh, var(--kmila-viewport-h, 80vh) - 16px);
}
.kmila-config-pill__panel,
.kmila-profile-menu__panel {
    max-height: min(80vh, var(--kmila-viewport-h, 80vh) - 16px);
}

/* Keyboard-open mode: hide bottom-anchored floating UI that's already
   covered by the keyboard. `.kmila-dock` and `.run-bar` are phone-only
   fixed-position surfaces (pages.css:9951, :10068) and the user cannot
   interact with them while the IME is open anyway — hiding them prevents
   the visual "peek behind the keyboard" flash on Android WebView. */
html[data-kmila-kbd="open"] .kmila-dock,
html[data-kmila-kbd="open"] .run-bar {
    display: none !important;
}

/* Bottom-sheet-styled schematic-info modal on phones (pages.css:10169)
   already caps at 50 dvh; when the keyboard is open, tighten to the
   visible band so its content stays scrollable rather than clipped. */
html[data-kmila-kbd="open"] .schematic-info {
    max-height: min(50dvh, var(--kmila-viewport-h, 50dvh) - 32px);
}

/* Blazor error banner sits `position: fixed; bottom: 0` on
   MainLayout.razor.css:14 — critical enough to still show over the
   keyboard rather than hide, but pin it above the IME so its Reload
   button stays tappable. `--kmila-viewport-h` gives the visible height;
   subtract from layout viewport to derive keyboard offset. When
   `data-kmila-kbd` is not set, the calc collapses to 0 so nothing moves. */
html[data-kmila-kbd="open"] #blazor-error-ui {
    bottom: calc(100dvh - var(--kmila-viewport-h, 100dvh));
}
