/* Kitae Studio - app-specific styles.
   The loading-screen (.app-loading) and fullscreen-IDE (body.cat-ide-fullscreen) styles
   both ship with the Catanari framework CSS, so nothing is needed here to get the IDE
   shell looking right. Add Kitae Studio-specific overrides below as the app grows. */

/* ── Boot loading bar ────────────────────────────────────────────────────────
   The app-loading overlay (framework CSS) centres this. One determinate bar,
   width driven by kitaestudio:progress events - see _Layout.cshtml. */
.app-loading-track {
    width: 280px;
    height: 6px;
    border-radius: 3px;
    background: var(--tblr-border-color, #e6e7e9);
    overflow: hidden;
    margin: 0 auto 0.75rem;
}

.app-loading-bar {
    height: 100%;
    width: 5%;
    border-radius: 3px;
    background: var(--tblr-purple, #7d3ec9);
    transition: width 250ms ease;
}

/* ── WalletDisplay bob (Stage 10 polish) ─────────────────────────────────────
   The three coin icons drift up and down at the SAME speed but with a different
   tween + phase each, so they never move in lockstep. Applied per-icon by
   WalletDisplay.bob().

   will-change promotes each icon onto its own compositor layer: without it the
   transform is applied during main-thread paint, which snaps positions to whole
   device pixels - a 3px drift over 2.6s then judders in visible 1px steps. On a
   compositor layer fractional offsets are resampled smoothly. */
@keyframes ks-wallet-bob {
    0%, 100% { transform: translate3d(0, 0, 0); }
    50%      { transform: translate3d(0, -3px, 0); }
}

.ks-wallet-bob-0 { will-change: transform; animation: ks-wallet-bob 2.6s ease-in-out infinite; }
/* NeonDisplay's orb: same drift, different duration + negative delay so the neon orb and the
   gold coin never bob in lockstep (their phases slowly slide past each other). */
.ks-wallet-bob-1 { will-change: transform; animation: ks-wallet-bob 2.9s ease-in-out -1.2s infinite; }

@media (prefers-reduced-motion: reduce) {
    .ks-wallet-bob-0, .ks-wallet-bob-1 { animation: none; }
}

/* ── About dialog credits crawl ──────────────────────────────────────────────
   A slow, seamless upward crawl inside an overflow-hidden viewport. AboutDialog
   renders [spacer + credits] twice back-to-back - the spacer is exactly one
   viewport tall (100cqh; the viewport is a size container), so the reel opens
   empty and the credits rise in from below the bottom edge, and translating the
   track by -50% still loops perfectly. (Hover-pause can't trigger inside the
   About overlay - it is pointer-transparent by design so the cursor reaches the
   live scene.) */
@keyframes ks-credits-crawl {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(0, -50%, 0); }
}

.ks-credits-viewport {
    position: relative;
    height: 170px;
    overflow: hidden;
    container-type: size;
    /* Soft fade at both ends so lines materialise rather than pop */
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 12%, #000 88%, transparent 100%);
    mask-image: linear-gradient(to bottom, transparent 0, #000 12%, #000 88%, transparent 100%);
}

.ks-credits-spacer {
    height: 100cqh;
}

.ks-credits-track {
    /* 56s keeps the old 36s crawl speed now each loop half also spans the spacer */
    animation: ks-credits-crawl 56s linear infinite;
    will-change: transform;
}

.ks-credits-viewport:hover .ks-credits-track {
    animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) {
    .ks-credits-track { animation: none; }
    /* No motion means the reel never rises in - show it statically from the top. */
    .ks-credits-spacer { display: none; }
}

/* ── About dialog: landscape hero stage ──────────────────────────────────────
   The modal body IS the scene: a fixed-height landscape stage with the live
   Kitae canvas filling it edge-to-edge (the browser happily composites DOM over
   a WebGL/WebGPU canvas) and the identity + credits sitting on top. The whole
   overlay is pointer-transparent so hover and clicks fall straight through to
   the canvas - that's how the scene (embers, vector balls, the wordmark
   letters) gets to react to the cursor. */
.ks-about-body {
    position: relative;
    height: 440px;
    overflow: hidden;
}

.ks-about-hero {
    position: absolute;
    inset: 0;
}

.ks-about-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: stretch;
    justify-content: space-between;
    gap: 1rem;
    padding: 1.25rem 1.5rem;
    /* Everything above the scene lets the cursor through to the canvas below. */
    pointer-events: none;
    /* Edge scrims keep the text legible over the brightest embers. */
    background: linear-gradient(90deg, rgba(9, 11, 22, 0.55) 0%, rgba(9, 11, 22, 0.15) 40%, rgba(9, 11, 22, 0) 55%, rgba(9, 11, 22, 0.45) 100%);
}

.ks-about-overlay * {
    pointer-events: none;
}

/* The scene is always a dark night sky whatever the app theme, so the overlay
   text is always light, with a soft shadow for lift. */
.ks-about-identity {
    align-self: flex-end;
    max-width: 55%;
    color: #eef0fa;
    text-shadow: 0 1px 10px rgba(0, 0, 0, 0.65);
}

.ks-about-identity h3 {
    color: inherit;
}

.ks-about-identity .text-secondary,
.ks-about-identity .text-muted {
    color: rgba(228, 231, 246, 0.78) !important;
}

.ks-about-credits {
    width: 280px;
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    color: #eef0fa;
    text-shadow: 0 1px 10px rgba(0, 0, 0, 0.65);
}

.ks-about-credits .ks-credits-viewport {
    height: 100%;
    flex: 1 1 auto;
}

.ks-about-credits .text-secondary {
    color: rgba(228, 231, 246, 0.7) !important;
}

/* ==================== Beta dialog (BetaDialog) ====================
   Reuses .ks-about-body / .ks-about-hero / .ks-about-overlay for the stage + scene.
   The beta manifesto sits in a panel on the RIGHT half of the stage (the wordmark
   column lives on the left of the canvas), always light on the dark night sky. */
.ks-beta-overlay {
    /* Text lives on the right, so scrim that side harder for legibility. */
    background: linear-gradient(90deg, rgba(6, 8, 20, 0) 0%, rgba(6, 8, 20, 0) 42%, rgba(6, 8, 20, 0.5) 62%, rgba(6, 8, 20, 0.82) 100%);
    align-items: center;
    justify-content: flex-end;
}

.ks-beta-panel {
    width: 52%;
    max-width: 500px;
    max-height: 100%;
    align-self: center;
    color: #eef0fa;
    text-shadow: 0 1px 12px rgba(0, 0, 0, 0.75);
}

/* The manifesto is the rendered `beta-welcome` markdown - a lot of text on a fixed-height
   stage - so the type and margins are tightened to always fit without spilling past the
   top/bottom of the scene. Styles target the Markdown control's generated elements. */
.ks-beta-panel h1,
.ks-beta-panel h2,
.ks-beta-panel h3 {
    /* "Welcome to the Beta" wears the current account theme's primary colour (the named themes
       swap --tblr-primary), lifted a touch for legibility over the dark night-sky scene. */
    color: var(--tblr-primary, #6c5ce7);
    font-size: 1.1rem;
    margin: 0 0 0.4rem 0;
}

.ks-beta-panel p {
    color: rgba(233, 236, 248, 0.92);
    font-size: 0.8rem;
    line-height: 1.3;
    margin: 0 0 0.45rem 0;
}

.ks-beta-panel p:last-child {
    margin-bottom: 0;
}

/* The closing italic line reads as the muted sign-off. */
.ks-beta-panel p em {
    color: rgba(228, 231, 246, 0.74);
    font-style: italic;
}

.ks-beta-panel strong {
    color: #fff;
    font-weight: 700;
}

.ks-beta-panel ul {
    margin: 0 0 0.4rem 0;
    padding-left: 1.05rem;
    color: rgba(233, 236, 248, 0.92);
    font-size: 0.77rem;
    line-height: 1.28;
}

.ks-beta-panel li {
    margin-bottom: 0.18rem;
}

/* ── Artwork mosaic (Project Properties → Artwork) ───────────────────────────
   MosaicGrid (src/881): a container-queried CSS grid of SQUARE cells - side =
   (container width − gaps) ÷ columns - onto which ArtworkTiles (src/882) are
   placed at solver-computed coordinates. The custom properties are set by the
   control; the container query keeps cells square at every width, so the pack
   scales proportionally and never develops holes. */

.ks-mosaic-host {
    container-type: inline-size;
}

.ks-mosaic {
    display: grid;
    grid-template-columns: repeat(var(--ks-mosaic-cols, 12), 1fr);
    grid-auto-rows: calc((100cqw - (var(--ks-mosaic-cols, 12) - 1) * var(--ks-mosaic-gap, 8px)) / var(--ks-mosaic-cols, 12));
    gap: var(--ks-mosaic-gap, 8px);
}

/* ── ArtworkTile - image frame, empty face, hover overlay ── */

.ks-art-tile {
    position: relative;
    overflow: hidden;
    border-radius: var(--tblr-border-radius, 4px);
    border: 1px solid var(--tblr-border-color);
    background: var(--tblr-bg-surface-tertiary, var(--tblr-bg-surface-secondary));
}

.ks-art-tile-img {
    position: absolute;
    inset: 0;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}

.ks-art-tile--empty .ks-art-tile-img {
    display: none;
}

/* The missing-artwork face: a big muted glyph + the slot's name, centred. */
.ks-art-tile-empty-face {
    position: absolute;
    inset: 0;
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    padding: 0.5rem;
    text-align: center;
    color: var(--tblr-secondary);
}

.ks-art-tile--empty .ks-art-tile-empty-face {
    display: flex;
}

/* While the overlay is up its title says the same thing - fade the face to a
   watermark so the slot name doesn't read twice. */
.ks-art-tile:hover .ks-art-tile-empty-face,
.ks-art-tile:focus-within .ks-art-tile-empty-face,
.ks-art-tile.cat-file-drop-over .ks-art-tile-empty-face {
    opacity: 0.3;
    transition: opacity 0.12s ease-in-out;
}

/* Tabler's `.icon` class boxes glyphs at 20×20 - at 2.4rem the glyph overflows the
   box and crowds the label below. Re-box at 1em with line-height 1 so the glyph is
   optically centred over the label (same doctrine as the TreeListView icon fix). */
.ks-art-tile-empty-face .ti {
    font-size: 2.4rem;
    width: 1em;
    height: 1em;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0.35;
}

.ks-art-tile-empty-label {
    font-size: 0.8125rem;
    opacity: 0.75;
}

/* The hover overlay: invisible until hover/focus/drag-over; a bottom-weighted
   gradient keeps the artwork legible behind the white info text. pointer-events
   stays off so clicks fall through to the drop zone (browse) - only the action
   buttons re-enable it. */
.ks-art-tile-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: 0.15rem;
    padding: 0.625rem;
    opacity: 0;
    transition: opacity 0.12s ease-in-out;
    background: linear-gradient(180deg, rgba(16, 19, 26, 0.35) 0%, rgba(16, 19, 26, 0.05) 40%, rgba(16, 19, 26, 0.82) 100%);
    pointer-events: none;
}

.ks-art-tile:hover .ks-art-tile-overlay,
.ks-art-tile:focus-within .ks-art-tile-overlay,
.ks-art-tile.cat-file-drop-over .ks-art-tile-overlay {
    opacity: 1;
}

.ks-art-tile-ratio {
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    padding: 0.125rem 0.4rem;
    border-radius: 999px;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.03em;
    color: #fff;
    background: rgba(16, 19, 26, 0.65);
    border: 1px solid rgba(255, 255, 255, 0.25);
}

.ks-art-tile-title {
    color: #fff;
    font-weight: 600;
    font-size: 0.875rem;
    line-height: 1.2;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
}

.ks-art-tile-ideal {
    color: rgba(255, 255, 255, 0.85);
    font-size: 0.75rem;
    line-height: 1.25;
}

.ks-art-tile-purpose {
    color: rgba(255, 255, 255, 0.62);
    font-size: 0.75rem;
    line-height: 1.25;
}

.ks-art-tile-actions {
    display: flex;
    gap: 0.375rem;
    margin-top: 0.3rem;
    pointer-events: auto;
}

/* Glassy Tabler buttons that read on any artwork. */
.ks-art-tile-actions .btn {
    --tblr-btn-color: #fff;
    --tblr-btn-bg: rgba(255, 255, 255, 0.14);
    --tblr-btn-border-color: rgba(255, 255, 255, 0.28);
    --tblr-btn-hover-color: #fff;
    --tblr-btn-hover-bg: rgba(255, 255, 255, 0.28);
    --tblr-btn-hover-border-color: rgba(255, 255, 255, 0.45);
    --tblr-btn-active-color: #fff;
    --tblr-btn-active-bg: rgba(255, 255, 255, 0.34);
    --tblr-btn-active-border-color: rgba(255, 255, 255, 0.5);
    backdrop-filter: blur(4px);
}

/* Drag-over: light the frame hard and force the overlay on, so the drop target
   is unmistakable mid-drag. */
.ks-art-tile.cat-file-drop-over {
    border-color: var(--tblr-primary);
    outline: 2px solid var(--tblr-primary);
    outline-offset: -2px;
}

/* ── SoundTile - the app's one sound surface (sound-bin-plan SB2) ──
   The three overlays (tick, name, actions) are hover-revealed with OPACITY, never
   `display` - an inline display:none loses to a utility class's !important (the
   documented Catanari trap). The "always on" rules come AFTER the :hover rules and
   carry equal specificity, so source order decides and the pinned state wins. */

.ks-sound-tile-tick,
.ks-sound-tile-name,
.ks-sound-tile-actions {
    opacity: 0;
    transition: opacity 0.12s ease-in-out;
}

.ks-sound-tile:hover .ks-sound-tile-tick,
.ks-sound-tile:hover .ks-sound-tile-name,
.ks-sound-tile:hover .ks-sound-tile-actions,
.ks-sound-tile:focus-within .ks-sound-tile-tick,
.ks-sound-tile:focus-within .ks-sound-tile-name,
.ks-sound-tile:focus-within .ks-sound-tile-actions {
    opacity: 1;
}

/* The ACTIVE tick is always visible - the point of a rack is seeing the chosen
   sound from across the board. A broken tile keeps its name readable for the same
   reason: you must be able to see WHAT you lost without hunting for it. */
.ks-sound-tile .ks-sound-tile-tick.is-active,
.ks-sound-tile.is-broken .ks-sound-tile-name {
    opacity: 1;
}

.ks-sound-tile-name {
    z-index: 5;
    max-width: calc(100% - 22px);
    padding: 0 4px 1px 4px;
    border-radius: 0 4px 0 4px;
    background: rgba(16, 19, 26, 0.55);
    color: #fff;
    pointer-events: auto;
}

[data-bs-theme='light'] .ks-sound-tile-name {
    background: rgba(255, 255, 255, 0.72);
    color: var(--tblr-body-color);
}

.ks-sound-tile-name-input {
    height: 18px;
    min-height: 0;
    padding: 0 3px;
    font-size: 11px;
}

/* The link glyph: this file lives elsewhere in the project (SB4). Always visible. */
.ks-sound-tile-link {
    position: absolute;
    right: 3px;
    bottom: 2px;
    z-index: 5;
    font-size: 12px;
    line-height: 1;
    opacity: 0.6;
    color: var(--tblr-secondary);
    pointer-events: none;
}

/* The broken face: named, red-framed, and it stays until the user removes it (SB7). */
.ks-sound-tile.is-broken {
    outline: 1px solid var(--tblr-red);
    outline-offset: -1px;
}

.ks-sound-tile-broken {
    padding: 4px;
    text-align: center;
}

.ks-sound-tile-broken .ti {
    font-size: 1.25rem;
    width: 1em;
    height: 1em;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
}

.ks-sound-tile-broken-label {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--tblr-red);
}

/* ── Sound Bin node - the whole face is the drop target (SB8) ──
   No permanent dropzone chrome: one overlay does both jobs. It is shown solid
   while the bin is EMPTY, and faded up on drag-over via `cat-file-drop-over`
   (the class FileDropZone toggles) the rest of the time. Opacity, never
   `display` - an inline display:none loses to a utility class's !important. */

.ks-soundbin-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
    grid-auto-rows: 56px;
    align-content: start;
    gap: 4px;
}

.ks-soundbin-single {
    display: flex;
}

.ks-soundbin-tile {
    border: 1px solid var(--tblr-border-color);
    background: var(--tblr-bg-surface-tertiary, var(--tblr-bg-surface-secondary));
}

.ks-soundbin-dropface {
    z-index: 6;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.12s ease-in-out;
    color: var(--tblr-secondary);
    border-radius: var(--tblr-border-radius, 4px);
    background: var(--tblr-bg-surface, rgba(16, 19, 26, 0.9));
}

.ks-soundbin-dropface .ti {
    font-size: 1.75rem;
    width: 1em;
    height: 1em;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0.5;
}

/* Empty bin: the face is the whole content, permanently (muted). */
.ks-soundbin-dropface.is-empty {
    opacity: 0.85;
}

/* Mid-drag: light the frame hard and force the face on, whichever of the three
   drag kinds is in flight (OS files, tree rows, asset links). */
.ks-soundbin-zone.cat-file-drop-over .ks-soundbin-dropface {
    opacity: 1;
}

.ks-soundbin-zone.cat-file-drop-over {
    outline: 2px solid var(--tblr-primary);
    outline-offset: -2px;
    border-radius: var(--tblr-border-radius, 4px);
}

/* ── Vector image node - the SVG render-down settings rollout ──
   The controls sit in an inset panel with breathing room, so a node reads as a
   picture with a settings drawer under it rather than as a bare stack of inputs
   jammed against the node's edges. */

.ks-vector-rollout {
    padding: 2px 4px 0 4px;
}

.ks-vector-settings {
    padding: 6px 8px 8px 8px;
    border-top: 1px solid var(--tblr-border-color);
}

/* Tabler's switch reserves a form-row's worth of margin; at node scale that is a
   gap between every toggle and no room for the picture. */
.ks-vector-settings .form-check {
    margin-bottom: 0;
    min-height: 0;
    display: flex;
    align-items: center;
    gap: 6px;
}

/* ── NineBoxCell - the UI Style editor's facet tile ──
   Same idiom as .ks-art-tile: the artwork sits alone in a clean frame and every
   control that describes it is hover-revealed. Ten cells each carrying a label,
   a thumbnail, a size caption, a select and a button made the artwork the
   smallest thing on screen; behind an overlay, the thumbnails read as a
   nine-box. Revealed on hover, focus-within (keyboard) and drag-over. */

.ks-ninebox-cell {
    position: relative;
    overflow: hidden;
    aspect-ratio: 1 / 1;
    border-radius: var(--tblr-border-radius, 4px);
    border: 1px solid var(--tblr-border-color);
    background: var(--tblr-bg-surface-tertiary, var(--tblr-bg-surface-secondary));
    cursor: pointer;
}

.ks-ninebox-cell:focus-visible {
    outline: 2px solid var(--tblr-primary);
    outline-offset: -2px;
}

/* An inherited state has no aspect to write into: the whole tile greys out and
   stops inviting clicks. */
.ks-ninebox-cell--disabled {
    opacity: 0.55;
    cursor: default;
}

.ks-ninebox-cell-art {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}

.ks-ninebox-cell--empty .ks-ninebox-cell-art {
    display: none;
}

/* The empty face: a muted glyph + the slot's name. A missing facet is legitimate
   - its band collapses to zero and the middle grows into it (UI4) - so this
   reads as "nothing here", never as an error. */
.ks-ninebox-cell-empty-face {
    position: absolute;
    inset: 0;
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    padding: 0.35rem;
    text-align: center;
    color: var(--tblr-secondary);
    font-size: 0.7rem;
}

.ks-ninebox-cell--empty .ks-ninebox-cell-empty-face {
    display: flex;
}

/* Tabler's `.icon` boxes glyphs at 20x20; re-box at 1em/line-height 1 so an
   enlarged glyph stays optically centred over its label (the TreeListView icon
   doctrine). */
.ks-ninebox-cell-empty-face .ti {
    font-size: 1.6rem;
    width: 1em;
    height: 1em;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0.35;
}

/* While the overlay is up its title says the same thing - fade the face to a
   watermark so the slot name does not read twice. */
.ks-ninebox-cell:hover .ks-ninebox-cell-empty-face,
.ks-ninebox-cell:focus-within .ks-ninebox-cell-empty-face,
.ks-ninebox-cell.cat-file-drop-over .ks-ninebox-cell-empty-face {
    opacity: 0.3;
    transition: opacity 0.12s ease-in-out;
}

.ks-ninebox-cell-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: 0.15rem;
    padding: 0.35rem;
    opacity: 0;
    transition: opacity 0.12s ease-in-out;
    background: linear-gradient(180deg, rgba(16, 19, 26, 0.35) 0%, rgba(16, 19, 26, 0.05) 40%, rgba(16, 19, 26, 0.85) 100%);
    /* Clicks fall through to the tile (which opens the picker); only the action
       row re-enables them. */
    pointer-events: none;
}

.ks-ninebox-cell:hover .ks-ninebox-cell-overlay,
.ks-ninebox-cell:focus-within .ks-ninebox-cell-overlay,
.ks-ninebox-cell.cat-file-drop-over .ks-ninebox-cell-overlay {
    opacity: 1;
}

/* A disabled tile still explains itself on hover, but its controls are dead. */
.ks-ninebox-cell--disabled .ks-ninebox-cell-actions {
    pointer-events: none;
}

/* The frame's natural size IS the band thickness this facet contributes (UI4),
   so it stays one glance away rather than hidden in a tooltip. */
.ks-ninebox-cell-size {
    position: absolute;
    top: 0.3rem;
    right: 0.3rem;
    padding: 0.05rem 0.3rem;
    border-radius: 999px;
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: #fff;
    background: rgba(16, 19, 26, 0.65);
    border: 1px solid rgba(255, 255, 255, 0.25);
}

.ks-ninebox-cell-title {
    color: #fff;
    font-weight: 600;
    font-size: 0.7rem;
    line-height: 1.2;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
}

.ks-ninebox-cell-actions {
    display: flex;
    gap: 0.2rem;
    align-items: center;
    margin-top: 0.15rem;
    pointer-events: auto;
}

.ks-ninebox-cell-actions .form-select {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 0.625rem;
    padding: 0.05rem 1.1rem 0.05rem 0.3rem;
    background-position: right 0.25rem center;
    color: #fff;
    background-color: rgba(255, 255, 255, 0.14);
    border-color: rgba(255, 255, 255, 0.28);
}

/* Glassy buttons that read against any artwork. */
.ks-ninebox-cell-actions .btn {
    --tblr-btn-color: #fff;
    --tblr-btn-bg: rgba(255, 255, 255, 0.14);
    --tblr-btn-border-color: rgba(255, 255, 255, 0.28);
    --tblr-btn-hover-color: #fff;
    --tblr-btn-hover-bg: rgba(255, 255, 255, 0.28);
    --tblr-btn-hover-border-color: rgba(255, 255, 255, 0.45);
    --tblr-btn-active-color: #fff;
    --tblr-btn-active-bg: rgba(255, 255, 255, 0.34);
    --tblr-btn-active-border-color: rgba(255, 255, 255, 0.5);
    padding: 0.05rem 0.3rem;
    backdrop-filter: blur(4px);
}

/* Drag-over: light the frame hard and force the overlay on, so the drop target
   is unmistakable mid-drag. */
.ks-ninebox-cell.cat-file-drop-over {
    border-color: var(--tblr-primary);
    outline: 2px solid var(--tblr-primary);
    outline-offset: -2px;
}

/* ── Light-grey badges must stay readable in the dark theme ──────────────────
   Tabler defines --tblr-gray-{50,100,200,300}-fg as var(--tblr-body-color), and
   the dark theme sets --tblr-body-color: var(--tblr-gray-200). The grey scale is
   NOT flipped for dark mode, so a `bg-gray-200 text-gray-200-fg` badge (what
   Catanari's Badge control emits for StatusColor.Gray200) renders light text on
   an identically light background - the ticket category / closed-status /
   low-priority / comment-count badges all came out blank.
   Those four backgrounds are light in BOTH themes, so pin their foregrounds to a
   dark ink instead of following the body colour. */
[data-bs-theme="dark"] {
    --tblr-gray-50-fg: var(--tblr-gray-900);
    --tblr-gray-100-fg: var(--tblr-gray-900);
    --tblr-gray-200-fg: var(--tblr-gray-900);
    --tblr-gray-300-fg: var(--tblr-gray-900);
}

/* ── Remote cursors in a shared code editor (collab-editing Stage 10) ────────
   y-monaco draws each remote peer's caret and selection as Monaco decorations
   classed `yRemoteSelection yRemoteSelection-<clientId>` and
   `yRemoteSelectionHead yRemoteSelectionHead-<clientId>`. It does NOT colour
   them or name them - the per-peer colour and label are the consumer's job, and
   SharedDocSession injects one rule per live peer for exactly that.

   What lives here is the shape those rules paint into: a translucent wash for a
   selection, a hairline caret, and a name chip that appears on hover rather than
   sitting permanently over the code the two of you are editing.

   Only Monaco is involved - no framework control renders any of this - so it
   belongs in the app's own stylesheet rather than in Catanari. */

.monaco-editor .yRemoteSelection {
    background-color: var(--tblr-primary);
    opacity: 0.3;
}

.monaco-editor .yRemoteSelectionHead {
    position: relative;
    border-left: 2px solid var(--tblr-primary);
    box-sizing: border-box;
}

.monaco-editor .yRemoteSelectionHead::after {
    content: "";
    position: absolute;
    top: -1.45em;
    left: -2px;
    padding: 0 4px;
    border-radius: 3px;
    font-size: 0.7rem;
    line-height: 1.4em;
    white-space: nowrap;
    color: #fff;
    background-color: var(--tblr-primary);
    opacity: 0;
    transition: opacity 120ms ease-in-out;
    pointer-events: none;
}

.monaco-editor .yRemoteSelectionHead:hover::after {
    opacity: 1;
}
