﻿/* ========================================
   iLoveMusic UI Layer (L3)
   ----------------------------------------
   The one place a view reaches for when Bootstrap has no class for what it
   needs. Everything here is built from the KERN tokens in iLoveMusic-tokens.css,
   so a view never restates a colour, a radius, a size or a spacing again.

   WHAT THIS FILE IS NOT:
   It is not a second component layer. iLoveMusic-custom.css already remaps every
   Bootstrap component onto the tokens (cards, modals, forms, tables, badges,
   dropdowns, accordions, toasts, pagination, nav). Do not restate any of that
   here - extend it there. This file only adds what neither Bootstrap nor that
   file provides.

   WHY EACH RULE EXISTS:
   Every utility below was counted in the codebase before it was written. The
   number in each comment is how many times that exact pattern was spelled out
   by hand across the views, which is the evidence that it is a shared pattern
   and not one view's preference.

   THIS FILE IS ADDITIVE.
   Nothing in it changes how any page looks today - no existing markup carries
   these classes yet. Appearance only changes as views are migrated onto them,
   one batch at a time, so a regression is always traceable to one batch.
   ======================================== */


/* ========== INTERACTION ==========
   `cursor: pointer` was written out 74 times (31 inline, 43 in view <style>
   blocks). It is the single most duplicated declaration in the app. */

.ilomu-clickable {
    cursor: pointer;
}

/* Drag handles: 23 hand-written `cursor: grab`, none of which also set the
   :active state, so a dragged item kept the open-hand cursor mid-drag. */
.ilomu-draggable {
    cursor: grab;
}

    .ilomu-draggable:active {
        cursor: grabbing;
    }

/* Hover lift - 16 hand-written `transform: translateY(-2px)`, each with its own
   transition or none at all. The shadow steps up with the movement, otherwise
   the element rises without casting anything and reads as a glitch. */
.ilomu-lift {
    transition: transform 0.15s ease, box-shadow 0.15s ease;
}

    .ilomu-lift:hover {
        transform: translateY(-2px);
        box-shadow: var(--ilomu-shadow-md);
    }

/* Respect a user who has asked the OS for reduced motion. The hand-written
   copies of the lift never did this. */
@media (prefers-reduced-motion: reduce) {
    .ilomu-lift {
        transition: none;
    }

        .ilomu-lift:hover {
            transform: none;
        }
}


/* ========== SCROLL PANES ==========
   `max-height: N; overflow-y: auto` appeared ~50 times, clustered on three
   heights: 400px (18x), 300px (7x) and ~200px. Three classes cover them.
   overscroll-behavior stops a pane at its own end instead of carrying the
   scroll on to the page behind it - none of the hand-written copies did that. */

.ilomu-scroll,
.ilomu-scroll-sm,
.ilomu-scroll-lg {
    overflow-y: auto;
    overscroll-behavior: contain;
}

.ilomu-scroll-sm {
    max-height: 200px;
}

.ilomu-scroll {
    max-height: 300px;
}

.ilomu-scroll-lg {
    max-height: 400px;
}

/* A themed scrollbar for those panes. Firefox takes the standard properties,
   WebKit needs its pseudo-elements; both read the same two tokens so they
   cannot disagree. Scoped to the panes rather than applied globally, so the
   browser's native scrollbar still governs the page itself. */
.ilomu-scroll,
.ilomu-scroll-sm,
.ilomu-scroll-lg {
    scrollbar-width: thin;
    scrollbar-color: var(--ilomu-border) transparent;
}

    .ilomu-scroll::-webkit-scrollbar,
    .ilomu-scroll-sm::-webkit-scrollbar,
    .ilomu-scroll-lg::-webkit-scrollbar {
        width: 8px;
        height: 8px;
    }

    .ilomu-scroll::-webkit-scrollbar-track,
    .ilomu-scroll-sm::-webkit-scrollbar-track,
    .ilomu-scroll-lg::-webkit-scrollbar-track {
        background: transparent;
    }

    .ilomu-scroll::-webkit-scrollbar-thumb,
    .ilomu-scroll-sm::-webkit-scrollbar-thumb,
    .ilomu-scroll-lg::-webkit-scrollbar-thumb {
        background: var(--ilomu-border);
        border-radius: var(--ilomu-radius-pill);
    }


/* ========== TEXT ==========
   Truncation was written out with three declarations every time. One class. */

.ilomu-truncate {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0; /* without this a truncating flex child refuses to shrink */
}

/* Multi-line clamp for descriptions and bios. */
.ilomu-clamp-2,
.ilomu-clamp-3 {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.ilomu-clamp-2 {
    -webkit-line-clamp: 2;
    line-clamp: 2;
}

.ilomu-clamp-3 {
    -webkit-line-clamp: 3;
    line-clamp: 3;
}

/* Fixed-width readouts: IDs, hashes, ISRCs, durations. Six different monospace
   stacks were spelled out across the views; they all become this. */
.ilomu-mono {
    font-family: var(--ilomu-font-mono);
    font-variant-numeric: tabular-nums;
}

/* Numbers in a column line up only with tabular figures. Any table cell or stat
   holding a number should carry this, and none of them did. */
.ilomu-num {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum";
}

/* Preserve the line breaks in server-produced text (job logs, diagnostics)
   without a <pre> and its UA margin and font. */
.ilomu-pre {
    white-space: pre-wrap;
    word-break: break-word;
}


/* ========== MEDIA ==========
   Avatars and cover thumbnails were sized by hand at 32px, 3rem, 50px, 60px and
   80px, usually with object-fit repeated alongside. One size token, three steps. */

.ilomu-avatar,
.ilomu-avatar-sm,
.ilomu-avatar-lg,
.ilomu-thumb,
.ilomu-thumb-sm,
.ilomu-thumb-lg {
    object-fit: cover;
    flex-shrink: 0; /* a media box in a flex row must never be squeezed */
    background-color: var(--ilomu-surface-elevated); /* holds the space while loading */
}

/* Round: people. */
.ilomu-avatar-sm {
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
}

.ilomu-avatar {
    width: 3rem;
    height: 3rem;
    border-radius: 50%;
}

.ilomu-avatar-lg {
    width: 4rem;
    height: 4rem;
    border-radius: 50%;
}

/* Square: artwork. Cover art is square by definition, so these are 1:1 and
   carry the app radius rather than a hand-picked 8px. */
.ilomu-thumb-sm {
    width: 2.5rem;
    height: 2.5rem;
    border-radius: var(--ilomu-radius);
}

.ilomu-thumb {
    width: 3.5rem;
    height: 3.5rem;
    border-radius: var(--ilomu-radius);
}

.ilomu-thumb-lg {
    width: 5rem;
    height: 5rem;
    border-radius: var(--ilomu-radius);
}


/* ========== SURFACES ==========
   A plain panel that is not a Bootstrap .card. Views built these by hand with
   `background: var(--ilomu-surface); border: 1px solid var(--ilomu-border);
   border-radius: 8px; padding: 16px` - four declarations, and the radius
   drifted between 3px, 8px and 12px across the copies. */

.ilomu-panel {
    background-color: var(--ilomu-surface);
    border: var(--ilomu-border-width) solid var(--ilomu-border);
    border-radius: var(--ilomu-radius-lg);
    padding: var(--ilomu-space-lg);
}

/* The recessed variant, for a panel's own header or footer strip. */
.ilomu-panel-sunken {
    background-color: var(--ilomu-surface-sunken);
    border: var(--ilomu-border-width) solid var(--ilomu-border);
    border-radius: var(--ilomu-radius-lg);
    padding: var(--ilomu-space-lg);
}

/* The coloured left edge that marks a card's category or state. Written out as
   `border-left: 4px solid` in eight places across seven views (UserAccounts,
   BotAttempts, GenreDetection, ReleaseHeat, PlaylistImport, Similarity,
   DatabaseAccess). The width was 4px in every copy; only the colour ever
   differed.

   The colour deliberately stays with the view: it encodes that view's data
   (account type, severity, provider), not a design decision. A view's own
   `border-left-color` rule still wins over this shorthand, so the existing
   modifier classes keep working untouched. */
.ilomu-stripe {
    border-left: 4px solid var(--ilomu-accent-text);
}

/* The four SEMANTIC stripe colours, as opposed to the per-view data colours
   above. These exist because 135 cards encoded their state by repainting the
   whole card - `card bg-success text-white`, `card border-danger` - which gave
   the app nineteen different-looking cards for one component. A card is one
   component; its state is an edge, not a new skin. Same four states the rest of
   the system already names, on the -text shades because a border is a contour
   and the fills are tuned to sit UNDER a label, not to be one. */
.ilomu-stripe-success { border-left-color: var(--ilomu-success-text); }
.ilomu-stripe-danger  { border-left-color: var(--ilomu-danger-text); }
.ilomu-stripe-warning { border-left-color: var(--ilomu-warning-text); }
.ilomu-stripe-info    { border-left-color: var(--ilomu-info-text); }
.ilomu-stripe-accent  { border-left-color: var(--ilomu-accent-text); }


/* ========== LOADING OVERLAY ==========
   The scrim that covers a panel while it loads. Six hand-written copies -
   UserAccounts, DataMaintenance/Duplicates, PlaylistImport/Index,
   PlaylistImport/DataRetrieval, Similarity and ar.css - each spelling out the
   same nine declarations.

   They disagreed on the one that matters. Three used --ilomu-scrim
   (rgba(0,0,0,0.35), the dimming scrim). Three used --ilomu-overlay-on-media,
   which is rgba(255,255,255,0.2) - a WHITE wash. Those three barely dimmed
   anything, and .ilomu-overlay-content in iLoveMusic-custom.css deliberately
   forces its spinner and label to white ("the scrim is semi-transparent
   black"), so in a light theme that label was white on near-white. All of them
   take the scrim now.

   `inset: 0` replaces the four separate offsets. `border-radius: inherit`
   replaces the hand-set 8px two of them carried, so the scrim follows whatever
   radius the panel it covers actually has. The covered element needs
   `position: relative`, which every caller already sets. */

.ilomu-overlay {
    position: absolute;
    inset: 0;
    z-index: 10;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--ilomu-scrim);
    border-radius: inherit;
}


/* ========== CONSOLE / LOG SURFACE ==========
   Eight admin views build a log viewer by hand, each with its own monospace
   stack, its own height and its own background - `#debug-console` in four
   training views alone. The colours are the fixed console palette from PART 3
   of the token file: a terminal reads the same in every theme on purpose. */

.ilomu-console {
    font-family: var(--ilomu-font-mono);
    font-size: var(--ilomu-fs-small);
    line-height: 1.5;
    background-color: var(--ilomu-console-bg);
    color: var(--ilomu-console-text);
    border: var(--ilomu-border-width) solid var(--ilomu-border);
    border-radius: var(--ilomu-radius);
    padding: var(--ilomu-space-lg);
    overflow: auto;
    overscroll-behavior: contain;
    white-space: pre-wrap;
    word-break: break-word;
    margin: 0; /* it is usually a <pre>, which carries a UA margin */
    /* A FIXED height, not a max: a log viewer starts empty and waiting, and a
       max-height box would sit collapsed to nothing until the first line
       arrives, then jump. Every hand-written copy used `height` for exactly
       that reason. The measured sizes were 400px (4x), 300px, 280px and 200px,
       so the height is a variable and the modifiers below set it. */
    height: var(--ilomu-console-height, 25rem); /* 400px */
}

.ilomu-console-sm {
    --ilomu-console-height: 12.5rem; /* 200px */
}

.ilomu-console-md {
    --ilomu-console-height: 18.75rem; /* 300px */
}

/* For the rare console that should grow with its content instead of holding a
   viewport - a short result dump rather than a live log. */
.ilomu-console-auto {
    height: auto;
    max-height: var(--ilomu-console-height, 25rem);
}


/* ========== LIVE LOG ==========
   The streaming log viewer, as opposed to the plain .ilomu-console above: it
   holds one element per line, each with a timestamp, a message and a severity.

   Five views built this - GenreDetection, CsvImport, PlaylistImport/DataRetrieval,
   SongStatistics and SongStatsEmbeddings - and the copies had drifted: the
   surface was --ilomu-bg in two and --ilomu-console-bg in two, the font size
   was 0.75rem or 0.8rem, and the height was 200px, 250px, 280px, 350px or
   350px. SongStatsEmbeddings had the most complete version and is the model.

   The class names are NOT ours to choose: the JS in all five builds them as
   `log-entry ${levelClass}` where levelClass is `log-` + the server's level
   name. Both spellings the server emits are supported (log-info from the
   detection views, log-information from the Serilog-backed ones). */

.ilomu-console-log {
    font-family: var(--ilomu-font-mono);
    font-size: var(--ilomu-fs-small);
    line-height: 1.4;
    background-color: var(--ilomu-console-bg);
    color: var(--ilomu-console-text);
    height: var(--ilomu-console-height, 25rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--ilomu-space);
    border-radius: var(--ilomu-radius);
    scroll-behavior: smooth;
}

    .ilomu-console-log .log-entry {
        padding: 2px var(--ilomu-space-2);
        margin: 1px 0;
        border-radius: calc(var(--ilomu-radius) / 2);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        /* No entry animation on the base, deliberately - see
           .ilomu-console-log--animate below. */
    }

/* The newest-line fade-in, opt-in.
   All five of these views poll on a timer and rebuild the whole log with
   innerHTML, so every line is a brand new element on every tick. An
   unconditional entry animation therefore replays across the entire console
   once or twice a second - a flicker, not a fade-in. SongStatsEmbeddings and
   SongStatistics already solved that in their JS: they stamp
   style="animation: none;" on every entry except the newest. Only those two
   carry this class, and their inline suppression still does the work. */
.ilomu-console-log--animate .log-entry {
    animation: ilomu-log-in 0.3s ease-in;
}

@keyframes ilomu-log-in {
    from { opacity: 0; transform: translateY(-5px); }
    to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
    .ilomu-console-log--animate .log-entry {
        animation: none;
    }
}

    .ilomu-console-log .log-timestamp {
        color: var(--ilomu-console-success);
        margin-right: var(--ilomu-space-2);
        font-weight: var(--ilomu-fw-medium);
    }

    .ilomu-console-log .log-message {
        color: var(--ilomu-console-text);
    }

    /* Severity.
       Every rule sets the colour on the ENTRY as well as on the nested
       .log-message, because the five views emit two different shapes:
         wrapped   <div class="log-entry log-error">
                     <span class="log-timestamp">..</span>
                     <span class="log-message">..</span></div>
         bare      <div class="log-entry log-error">
                     <span style="color:..">[ts]</span> message text</div>
       A rule written only for .log-message silently does nothing on the bare
       shape - the message is a text node with no element to hook. Colouring
       the entry covers both; .log-timestamp carries its own colour and still
       wins where it exists. */
    .ilomu-console-log .log-trace { background-color: var(--ilomu-console-trace-bg); }
    .ilomu-console-log .log-trace,
    .ilomu-console-log .log-trace .log-message { color: var(--ilomu-console-text-muted); }

    .ilomu-console-log .log-debug { background-color: var(--ilomu-console-debug-bg); }
    .ilomu-console-log .log-debug,
    .ilomu-console-log .log-debug .log-message { color: var(--ilomu-console-text-muted); }

    .ilomu-console-log .log-info,
    .ilomu-console-log .log-information { background-color: var(--ilomu-console-info-bg); }
    .ilomu-console-log .log-info,
    .ilomu-console-log .log-information,
    .ilomu-console-log .log-info .log-message,
    .ilomu-console-log .log-information .log-message { color: var(--ilomu-console-info); }

    .ilomu-console-log .log-success,
    .ilomu-console-log .log-success .log-message { color: var(--ilomu-console-success); }

    .ilomu-console-log .log-warning { background-color: var(--ilomu-console-warning-bg); }
    .ilomu-console-log .log-warning,
    .ilomu-console-log .log-warning .log-message,
    .ilomu-console-log .log-warning .log-timestamp { color: var(--ilomu-console-warning); }

    .ilomu-console-log .log-error { background-color: var(--ilomu-console-error-bg); }
    .ilomu-console-log .log-error,
    .ilomu-console-log .log-error .log-message,
    .ilomu-console-log .log-error .log-timestamp { color: var(--ilomu-console-error); }

    .ilomu-console-log .log-critical { background-color: var(--ilomu-console-critical-bg); }
    .ilomu-console-log .log-critical,
    .ilomu-console-log .log-critical .log-message {
        color: var(--ilomu-console-critical);
        font-weight: var(--ilomu-fw-heading);
    }
    .ilomu-console-log .log-critical .log-timestamp { color: var(--ilomu-console-critical); }

    /* The terminal keeps its own scrollbar rather than the app one, because it
       sits on the fixed console palette and the app scrollbar would read as a
       different surface. Tokens exist for exactly this. */
    .ilomu-console-log {
        scrollbar-width: thin;
        scrollbar-color: var(--ilomu-console-scrollbar-thumb) var(--ilomu-console-scrollbar-track);
    }

    .ilomu-console-log::-webkit-scrollbar { width: 10px; }
    .ilomu-console-log::-webkit-scrollbar-track { background: var(--ilomu-console-scrollbar-track); }
    .ilomu-console-log::-webkit-scrollbar-thumb {
        background: var(--ilomu-console-scrollbar-thumb);
        border-radius: var(--ilomu-radius-pill);
    }
    .ilomu-console-log::-webkit-scrollbar-thumb:hover { background: var(--ilomu-console-scrollbar-thumb-hover); }


/* ========== CONSOLE DRAWER ==========
   The log viewer docked to the bottom of the viewport, collapsible by clicking
   its header. Three views built it - GenreDetection, CsvImport and
   PlaylistImport/DataRetrieval - and the copies disagreed on every number:
   collapsed 36 / 40 / 50px, expanded 200 / 250 / 280px, z-index 1000 / 1050,
   and only DataRetrieval remembered that the app sidebar is 260px wide, so the
   other two ran their drawer underneath it.

   The two toggle dialects both work here on purpose, so no JavaScript had to
   change: GenreDetection toggles only `expanded` against a collapsed base,
   while the other two swap `expanded` and `collapsed`. The base height is the
   collapsed one, so both end up in the same place.

   Height is flexbox rather than the `calc(100% - 45px)` each copy carried,
   which had to be kept in sync with the header by hand and was already wrong
   in one of them. */

.ilomu-console-drawer {
    position: fixed;
    bottom: 0;
    /* The workspace sidebar is 260px; --sidebar-width comes from workspace.css.
       The fallback is 0 for pages rendered without the app shell. */
    left: var(--sidebar-width, 0);
    right: 0;
    z-index: 1050;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background-color: var(--ilomu-surface);
    border-top: 2px solid var(--ilomu-border);
    transition: height 0.3s ease;
    height: var(--ilomu-console-drawer-collapsed, 2.5rem);
}

    .ilomu-console-drawer.collapsed {
        height: var(--ilomu-console-drawer-collapsed, 2.5rem);
    }

    .ilomu-console-drawer.expanded {
        height: var(--ilomu-console-drawer-height, 17.5rem);
    }

    .ilomu-console-drawer__header {
        flex: 0 0 auto;
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: var(--ilomu-space);
        padding: var(--ilomu-space) var(--ilomu-space-lg);
        background-color: var(--ilomu-surface-sunken);
        border-bottom: var(--ilomu-border-width) solid var(--ilomu-border);
        color: var(--ilomu-text);
        font-size: var(--ilomu-fs-small);
        cursor: pointer;
        user-select: none; /* the header is a click target; dragging it should not select its label */
    }

    /* The log fills whatever the header leaves, in every drawer height. */
    .ilomu-console-drawer .ilomu-console-log {
        flex: 1 1 auto;
        height: auto;
        min-height: 0; /* without this a flex child refuses to shrink below content */
        border-radius: 0;
    }

/* Put this on the page's content container so the drawer does not sit on top of
   the last rows. Each view carried its own padding-bottom guess (300px in one,
   nothing in the others). */
.ilomu-console-drawer-offset {
    padding-bottom: calc(var(--ilomu-console-drawer-height, 17.5rem) + var(--ilomu-space-lg));
}

@media (prefers-reduced-motion: reduce) {
    .ilomu-console-drawer {
        transition: none;
    }
}


/* ========== ANIMATIONS ==========
   Three keyframe sets were copied across the views: spin in seven, pulse in
   five, fadeIn in the log viewers. Identical definitions, redeclared each time.
   All three honour prefers-reduced-motion here, which none of the copies did. */

@keyframes ilomu-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

@keyframes ilomu-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
}

.ilomu-spin {
    animation: ilomu-spin 1s linear infinite;
}

.ilomu-pulse {
    animation: ilomu-pulse 1.5s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
    .ilomu-spin,
    .ilomu-pulse,
    .ilomu-preview-btn.playing {
        animation: none;
    }
}


/* ========== EMPTY STATE ==========
   `text-align:center; color:var(--...-text-muted); padding:16px` appeared as a
   hand-written trio for "nothing here yet" panels. */

.ilomu-empty {
    text-align: center;
    color: var(--ilomu-text-muted);
    padding: calc(var(--ilomu-space-lg) * 2) var(--ilomu-space-lg);
}

    .ilomu-empty .bi {
        font-size: var(--ilomu-fs-page-title);
        opacity: 0.5;
        display: block;
        margin-bottom: var(--ilomu-space);
    }


/* ========== THE BACK LINK ==========

   ARComposer/Details and ARComposer/Network spelled this out character for
   character in their own <style> blocks. It belongs here rather than in ar.css
   because neither of those two views links a stylesheet at all - they hang off
   _Layout, and the shared layer is the only file that reaches them. Checked
   before moving it, not assumed.

   The colour is the one both views chose. It is a decision about how the A&R
   area looks, not about how a link looks, so it is not being quietly changed
   to --ilomu-link-fg here. */

.back-link {
    color: var(--ilomu-warning-text);
    text-decoration: none;
}

.back-link:hover {
    text-decoration: underline;
}


/* ========== THE THIN PROGRESS BAR ==========

   Four views wrote this out, three of them identically:

     AppDiagnostics/Index        height: 6px
     ARComposer/Details          height: 6px
     DatabaseDiagnostics/Index   height: 6px
     DataMaintenance/PopularityIndex  height: 8px, plus a radius

   One height, and the radius comes along - a 6px bar with square ends next to
   a rounded .progress was the odd one out either way. */

.progress-thin {
    /* ui-scan-ok: the bar's own thickness, a control dimension. It tracks
       neither body type nor an icon, so no role on either scale fits. */
    height: 6px;
    border-radius: var(--ilomu-radius-sm);
}


/* ========== THE PREVIEW / PLAY CONTROL ==========

   A 32px square that plays a track preview, and JS toggles `.playing` on it
   while the audio runs. Three views build it, and all three disagreed:

     Similarity/Similarities   min-width 32  radius-lg  hover scale(1.1)
                               .playing -> a box-shadow ring keyframe
     SongAnalysis/Index        width 32      no radius  hover scale(1.1)
                               .playing -> a transform-scale keyframe
     AR/Index (ar.js)          .brand-btn gives the 32px box and the brand
                               fill - but NOTHING defines .playing there, so
                               ar.js has been setting a class that renders
                               nothing since it was written

   Two hand-written pulse keyframes for one state, and neither honoured
   prefers-reduced-motion. The app already has one pulse and it does; .playing
   uses it, so the third view gets a visible playing state for the first time.

   Note this class is a MODIFIER: twelve of the thirteen sites already carry
   .btn, and the AR one carries .brand-btn. It adds the box, the hover and the
   state, not a button. */

.ilomu-preview-btn {
    /* ui-scan-ok: 32px is the control's own box, the size of the tap target -
       it does not track body type, so a type or icon role would be the wrong
       scale. All three views wrote the same 32. */
    min-width: 32px;
    height: 32px;
    padding: 0 var(--ilomu-space-2);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--ilomu-radius-lg);
    white-space: nowrap;
    transition: transform 0.2s ease;
}

.ilomu-preview-btn:hover {
    transform: scale(1.1);
}

.ilomu-preview-btn.playing {
    animation: ilomu-pulse 1.5s ease-in-out infinite;
}


/* ========== THE LITTLE INFO GLYPH NEXT TO A LABEL ==========

   The same button - a bi-info-circle that opens an explanation modal - existed
   five times under two names (.pop-info-btn and .info-btn), and every copy
   agreed on the construction and disagreed on the three values:

     Analytics/Index        0 0.3rem    fs-body           hover accent-text
     RadioFitArtist/Index   0 0.3rem    fs-small          hover --bs-info
     SongAnalysis/Index     0 space-2   fs-section-title  hover accent-text
     ar-lyricists.css       0 0.35rem   fs-card-title     hover --bs-warning
     song-detail.css        0 0.15rem   fs-body           (inherited)

   plus one inline `padding:0 0.3rem` in songanalysis.js. Four paddings, four
   sizes, three hover colours, for one glyph doing one job.

   The size is on the ICON scale, not the type scale: it is a glyph, so it must
   not grow when someone enlarges a heading role. That is the same distinction
   74fd143d drew for the other 105 icon sizes.

   The hover is accent-text everywhere. --bs-info and --bs-warning do resolve to
   the app's tokens, but those are the FILL colours - the same "a fill is not a
   label" rule that gave success and danger their own -text variants. */

.ilomu-info-btn {
    background: transparent;
    border: none;
    padding: 0 var(--ilomu-space-1);
    color: var(--ilomu-text-muted);
    cursor: pointer;
    font-size: var(--ilomu-icon-sm);
    line-height: 1;
    vertical-align: middle;
}

.ilomu-info-btn:hover,
.ilomu-info-btn:focus-visible {
    color: var(--ilomu-accent-text);
}


/* ========== ACCORDION HEADER THAT CARRIES A DRAG HANDLE ==========

   Bootstrap paints the header's surface on .accordion-button, not on
   .accordion-header. A sortable accordion needs a grip that is NOT the toggle -
   clicking it must not open the panel - so the grip is a sibling of the button,
   and it therefore sits on the item's own background and reads as a pale gutter
   beside the row.

   The header takes the button's two surface tokens instead and follows the same
   two states, so the row is one colour again. Only the two tokens are named
   here; both are already what .accordion-button itself resolves to. */

.ilomu-accordion-drag {
    background-color: var(--ilomu-accordion-btn-bg);
    color: var(--ilomu-accordion-btn-fg);
    padding-left: var(--ilomu-accordion-btn-padding-x);
    border-top-left-radius: inherit;
    border-top-right-radius: inherit;
}

.ilomu-accordion-drag:has(.accordion-button:not(.collapsed)) {
    background-color: var(--ilomu-accordion-btn-active-bg);
    color: var(--ilomu-accordion-btn-active-fg);
}

/* The button brings its own left padding, and next to a grip that reads as a
   double indent. */
.ilomu-accordion-drag .accordion-button {
    padding-left: 0;
}


/* ========== COVER WITH A PLAY BUTTON ON IT ==========

   Four partials rendered on the same page each carried this structure, and two
   of them (_XP_WhatsHot.css and _XP_NewMusic.css) were character-identical for
   all thirteen declarations - the files differ only in their carousel ids. What
   varies between the contexts is the SIZE of the cover and of the glyph on it,
   and nothing else, so the size stays with the context and the structure lives
   here.

   The focus rule is the point of doing this at all. The play button is not a
   .btn, so the app's ring never reached it, and both _SongListSubView.css and
   _PlayerFooter.css had gone one step further and killed the UA outline with
   `outline: none !important` on :focus - so a keyboard user had no indicator on
   any of them. The ring is opacity-bound too: the button rests at opacity 0, and
   opacity applies to the box-shadow as well, so the ring is only visible if the
   button becomes visible with it. */

.songlist-cover-container {
    position: relative;
}

.songlist-cover-art {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.songlist-play-btn {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    margin: 0;
    background-color: transparent;
    border: none;
    color: var(--ilomu-text);
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.2s ease;
}

.songlist-cover-container:hover .songlist-play-btn,
.songlist-play-btn.playing {
    opacity: 1;
}

.songlist-play-btn:focus-visible {
    opacity: 1;
    box-shadow: 0 0 0 var(--ilomu-focus-ring-width) var(--ilomu-accent-focus-ring);
    outline: none;
}
