/* ===== Metrunome — sleek, dark, coral accent ===== */

/* One variable font instead of the four static weights Google was serving. Slightly larger than a
   latin-only subset, and worth it: the full charset means artist names in Greek, Cyrillic or Vietnamese
   render in Inter rather than dropping to a system fallback mid-playlist. SIL Open Font License, kept
   alongside the file. */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('/vendor/fonts/InterVariable.woff2') format('woff2');
}

:root {
    --bg: #0f1116;
    --surface: #181b22;
    --surface-2: #21252e;
    --border: #2a2f3a;
    --text: #f3f4f7;
    --text-muted: #969cab;
    --accent: #ef4136;
    --accent-hover: #f65a50;
    --accent-soft: rgba(239, 65, 54, 0.15);
    --on-accent: #ffffff;
    --danger: #ff6b6b;
    --warning: #e8b44a;
    --warning-soft: rgba(232, 180, 74, 0.12);
    --radius: 12px;
}

html {
    background-color: var(--bg);
    /* The full-bleed hero is sized with 100vw, which includes a classic
       scrollbar's width; without this that spills into a horizontal scrollbar. */
    overflow-x: hidden;
}

body {
    background-color: var(--bg);
    color: var(--text);
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
}

/* Typography */
.title {
    color: var(--text);
    font-weight: 800;
    letter-spacing: -0.025em;
}
.subtitle {
    color: var(--text-muted);
    font-weight: 400;
}
strong,
.subtitle strong,
.title strong,
.content strong,
p strong {
    color: var(--text);
}
.has-text-grey, .has-text-grey-light, .help {
    color: var(--text-muted) !important;
}
.has-text-danger {
    color: var(--danger) !important;
}
.has-text-link, .has-text-info {
    color: var(--accent) !important;
}
.has-text-success {
    color: #43d9a3 !important;
}
a {
    color: var(--accent);
    transition: color 0.15s ease;
}
a:hover {
    color: var(--accent-hover);
}

/* Header */
.hero.is-primary {
    background: linear-gradient(180deg, #171a21 0%, var(--bg) 100%);
    box-shadow: inset 0 -1px 0 var(--border);
}
.hero.is-primary .title,
.hero.is-primary .subtitle {
    color: var(--text);
}
.hero.is-primary a {
    color: var(--text-muted);
}
.hero.is-primary a:hover {
    color: var(--text);
}
.site-logo {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
}
.site-logo .wordmark {
    font-weight: 800;
    font-size: 1.45rem;
    letter-spacing: -0.035em;
    color: var(--text);
}
.site-logo:hover .wordmark {
    color: var(--text);
}
.hero.is-primary .nav-link {
    color: var(--text-muted);
    padding: 0.4rem 0.8rem;
    border-radius: 8px;
    font-weight: 500;
    transition: color 0.15s ease, background-color 0.15s ease;
}
.hero.is-primary .nav-link:hover {
    color: var(--text);
    background: rgba(255, 255, 255, 0.06);
}
.hero.is-primary .nav-link.is-nav-active {
    color: var(--accent);
    background: rgba(239, 65, 54, 0.16);
    box-shadow: inset 0 0 0 1.5px var(--accent);
    font-weight: 600;
}
/* tighten nav spacing so the items sit as a group */
.hero.is-primary .level-left .level-item:not(:first-child),
.hero.is-primary .level-right .level-item {
    margin-left: 0.15rem;
    margin-right: 0.15rem;
}

/* Boxes / cards */
.box {
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: none;
}

/* Buttons */
.button {
    background: var(--surface-2);
    border-color: var(--border);
    color: var(--text);
    border-radius: 9px;
    font-weight: 500;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}
.button:hover {
    background: #272c37;
    border-color: #3a4150;
    color: var(--text);
}
.button.is-primary,
.button.is-success {
    background: var(--accent);
    border-color: transparent;
    color: var(--on-accent);
}
.button.is-primary:hover,
.button.is-success:hover {
    background: var(--accent-hover);
    color: #fff;
}
.button.is-static {
    background: var(--surface);
    color: var(--text-muted);
    border-color: var(--border);
}

/* Inputs / selects */
.input, .textarea, .select select {
    background: var(--surface-2);
    border-color: var(--border);
    color: var(--text);
    border-radius: 9px;
    box-shadow: none;
}
.input::placeholder {
    color: #6b7280;
}
.input:focus, .textarea:focus, .select select:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}
.select:not(.is-multiple):not(.is-loading)::after {
    border-color: var(--accent);
}
.label {
    color: var(--text);
}
.checkbox:hover {
    color: var(--text);
}

/* Tags */
.tag:not(body) {
    background: var(--surface-2);
    color: var(--text-muted);
}
.tag.is-primary, .tag.is-primary.is-light {
    background: var(--accent-soft) !important;
    color: var(--accent) !important;
}

/* Tables (admin) */
.table {
    background: transparent;
    color: var(--text);
}
.table th {
    color: var(--text-muted);
    border-color: var(--border) !important;
}
.table td {
    border-color: var(--border) !important;
    color: var(--text);
}
.table.is-hoverable tbody tr:hover {
    background: var(--surface);
}

/* Modal */
.modal-background {
    background-color: rgba(6, 8, 12, 0.8);
}
.modal-card-head, .modal-card-foot {
    background: var(--surface);
    border-color: var(--border);
}
.modal-card-body {
    background: var(--surface);
    color: var(--text);
}
.modal-card-title {
    color: var(--text);
}

/* Footer */
.footer {
    background: var(--bg);
    color: var(--text-muted);
    box-shadow: inset 0 1px 0 var(--border);
    margin-top: auto;
}
.footer a {
    color: var(--text-muted);
    text-decoration: underline;
}
.footer a:hover {
    color: var(--text);
}

/* Media (playlist cards) */
.media + .media {
    border-top-color: var(--border);
}

/* Settings sections */
.settings-sections {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

/* ===== Loading states ===== */

/* Skeleton placeholders: sit inside a container until htmx swaps in the real content.
   The fade-in is delayed so fast responses never flash a skeleton. */
@keyframes skeleton-sheen {
    from { background-position: 200% 0; }
    to { background-position: -200% 0; }
}
@keyframes skeleton-appear {
    from { opacity: 0; }
    to { opacity: 1; }
}
.skeleton {
    background: linear-gradient(90deg, var(--surface-2) 25%, #2e3440 50%, var(--surface-2) 75%);
    background-size: 200% 100%;
    border-radius: 6px;
    animation: skeleton-sheen 1.4s ease-in-out infinite,
               skeleton-appear 0.25s ease-out 0.2s both;
}
.skeleton-line {
    height: 0.8rem;
    margin-bottom: 0.65rem;
}
.skeleton-line:last-child {
    margin-bottom: 0;
}
.skeleton-title {
    height: 1.5rem;
    border-radius: 8px;
    margin-bottom: 0.9rem;
}
.skeleton-control {
    height: 2.5rem;
    width: 12rem;
    border-radius: 9px;
    margin: 0.9rem 0;
}
.skeleton-thumb {
    width: 64px;
    height: 64px;
    border-radius: 10px;
    margin: 0.5rem;
}
.skeleton-chart {
    height: 120px;
    border-radius: 8px;
}

/* Spinner + inline "loading" row, for spots too small for a skeleton */
@keyframes spinner-rotate {
    to { transform: rotate(360deg); }
}
.spinner {
    display: inline-block;
    width: 1.15rem;
    height: 1.15rem;
    border: 2px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 9999px;
    animation: spinner-rotate 0.7s linear infinite;
}
.spinner.is-small {
    width: 0.85rem;
    height: 0.85rem;
}
.loading-inline {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    color: var(--text-muted);
    font-size: 0.875rem;
    padding: 0.5rem 0;
}

/* Buttons and submitting forms spin while their htmx request is in flight.
   Links that only close a modal (hx-get="javascript:void(0);") are excluded. */
.button.htmx-request:not([hx-get^="javascript"]),
form.htmx-request button[type="submit"] {
    color: transparent !important;
    pointer-events: none;
    position: relative;
}
.button.htmx-request:not([hx-get^="javascript"])::after,
form.htmx-request button[type="submit"]::after {
    content: "";
    position: absolute;
    top: calc(50% - 0.5em);
    left: calc(50% - 0.5em);
    width: 1em;
    height: 1em;
    border: 2px solid rgba(255, 255, 255, 0.3);
    border-top-color: #fff;
    border-radius: 9999px;
    animation: spinner-rotate 0.7s linear infinite;
}

/* Light-background buttons need a dark spinner to stay visible */
.button.is-light.htmx-request::after,
.button.is-white.htmx-request::after,
form.htmx-request button[type="submit"].is-light::after {
    border-color: rgba(0, 0, 0, 0.18);
    border-top-color: rgba(0, 0, 0, 0.6);
}

/* Per-track row actions spin in place. They aren't Bulma .buttons, so the rule above doesn't reach
   them, and their spinner takes the row's own colour rather than white — these sit on a dark table,
   not on a filled button. Replacing a song can take seconds, so without this the row looks dead. */
/* The heart is excluded: it answers optimistically, so replacing the new state with a spinner would
   undo the very thing that makes it feel instant. */
.row-action.htmx-request:not(.js-liked-toggle),
.card-action.htmx-request {
    color: transparent !important;
    pointer-events: none;
    position: relative;
}
.row-action.htmx-request:not(.js-liked-toggle)::after,
.card-action.htmx-request::after {
    content: "";
    position: absolute;
    top: calc(50% - 0.45em);
    left: calc(50% - 0.45em);
    width: 0.9em;
    height: 0.9em;
    border: 2px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 9999px;
    animation: spinner-rotate 0.7s linear infinite;
}

/* While one row action is running, the others stop responding. Replacing a song can take up to twelve
   seconds, and every action re-renders the whole list — two in flight at once would race, and the loser
   would silently overwrite the winner. htmx 1.7 has no hx-disabled-elt, hence :has(); browsers without
   it still get the spinner, just without the guard. */
.track-list:has(.row-action.htmx-request:not(.js-liked-toggle)) .row-action {
    pointer-events: none;
}
.track-list:has(.row-action.htmx-request:not(.js-liked-toggle)) .row-action:not(.htmx-request) {
    opacity: 0.35;
}

/* Container that dims while it is being refreshed in place */
.is-loading-fade.htmx-request {
    opacity: 0.4;
    pointer-events: none;
    transition: opacity 0.15s ease;
}

/* Top progress bar for full-page (nav) swaps */
@keyframes page-progress-slide {
    from { transform: translateX(-100%); }
    to { transform: translateX(350%); }
}
.page-progress {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 3px;
    z-index: 60;
    overflow: hidden;
    pointer-events: none;
}
.page-progress::after {
    content: "";
    display: block;
    height: 100%;
    width: 30%;
    background: linear-gradient(90deg, transparent, var(--accent), transparent);
    animation: page-progress-slide 1.1s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
    .skeleton {
        animation: skeleton-appear 0.25s ease-out 0.2s both;
        background: var(--surface-2);
    }
    .spinner,
    .button.htmx-request::after,
    .row-action.htmx-request::after,
    .card-action.htmx-request::after,
    form.htmx-request button[type="submit"]::after,
    .page-progress::after {
        animation-duration: 2.5s;
    }
}

/* Prose pages: a readable measure and room to breathe between sections. */
.legal {
    max-width: 46rem;
}

.legal h2.title {
    margin-top: 2.25rem;
}

/* Bulma pulls a .title following a .subtitle up by 1.25rem, which squashes the first heading against
   the intro. Its selector scores (0,3,0), so undoing it takes the adjacency too — (0,3,1). */
.legal .subtitle + h2.title {
    margin-top: 2.25rem;
}

.legal p,
.legal ul {
    color: var(--text-muted);
    margin-bottom: 0.75rem;
}

.legal ul {
    list-style: disc;
    padding-left: 1.25rem;
}

.legal li {
    margin-bottom: 0.4rem;
}

/* ===== app-specific ===== */

/* Card actions are always on screen. Revealing them on hover changed the row's height, which shoved the
   shortfall warning underneath around as the pointer moved — and hid the actions entirely on touch. */
.playlist-buttons {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

/* Matching squares rather than a mix of a filled label, an icon-and-text and a red icon: three actions
   of the same weight should look like a set. Muted until hovered, so an always-visible row of controls
   sits quietly next to the playlist name instead of competing with it. */
.card-action {
    align-items: center;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 10px;
    color: var(--text-muted);
    cursor: pointer;
    display: inline-flex;
    height: 2.25rem;
    justify-content: center;
    transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
    width: 2.25rem;
}

.card-action:hover {
    background: #272c37;
    border-color: #3a4150;
    color: var(--text);
}

/* Opening the playlist is the one thing most people came to do, so it keeps the accent. */
.card-action.is-primary-action {
    background: var(--accent);
    border-color: transparent;
    color: var(--on-accent);
}

.card-action.is-primary-action:hover {
    background: var(--accent-hover);
    color: #fff;
}

.card-action.is-danger-action:hover {
    border-color: var(--danger);
    color: var(--danger);
}
.media.is-align-items-center {
    align-items: center;
}
.duration-input {
    min-width: 6.5rem;
}

/* The keep/replace counts in the update confirmation. */
.update-summary {
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.75rem 1rem;
}

.update-summary div {
    align-items: center;
    display: flex;
    gap: 0.5rem;
    padding: 0.15rem 0;
}

/* The Liked-Songs heart in a playlist's track list. Its column keeps its width whether or not
   the row has a heart, so the titles stay in one line down the table. */
.preview-cell,
.liked-cell {
    width: 2rem;
    padding-right: 0 !important;
}

.row-action.is-playing {
    color: var(--accent);
}

/* The embed sits in a row of its own under the track it belongs to, flush so the two read as one item. */
.preview-row td {
    border: none !important;
    padding: 0 0 0.5rem !important;
}

.preview-row iframe {
    border: none;
    border-radius: var(--radius);
    display: block;
}

/* Per-track controls. Ghost buttons so three of them in a row read as one quiet cluster rather than
   three competing calls to action; each colours in only on hover, so the destructive one announces
   itself at the moment it could be pressed. */
.row-action {
    background: none;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
    line-height: 1;
    padding: 0.2rem 0.3rem;
    transition: color 0.15s ease;
}

.row-action:hover {
    color: var(--text);
}

.row-action.is-danger-action:hover {
    color: var(--danger);
}

.row-action.liked,
.row-action.liked:hover {
    color: var(--accent);
}

.row-actions {
    white-space: nowrap;
    width: 4.5rem;
}

.liked {
    color: var(--accent);
}

/* Admin quality tiles — fill rate and ban rate. Deliberately plain: two numbers whose
   meaning is easy to get wrong, so each carries its reading right underneath it. */
.stat-row {
    display: grid;
    gap: 1rem;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}

.stat {
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1rem 1.15rem;
}

.stat-label {
    color: var(--text-muted);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.stat-value {
    font-size: 2rem;
    font-weight: 600;
    line-height: 1.2;
}

.stat-value.is-empty {
    color: var(--text-muted);
}

.stat-note {
    color: var(--text-muted);
    font-size: 0.85rem;
}

.stat-help {
    border-top: 1px solid var(--border);
    color: var(--text-muted);
    font-size: 0.75rem;
    margin-top: 0.75rem;
    padding-top: 0.6rem;
}

/* A finished playlist that delivered less than was asked for. Deliberately not styled as an error —
   the playlist works, it's just shorter than requested — but visible enough that nobody has to
   compare "18 min" against what they typed to notice. */
.shortfall {
    align-items: center;
    flex-wrap: wrap;
    background: var(--warning-soft);
    border-left: 2px solid var(--warning);
    border-radius: 0 var(--radius) var(--radius) 0;
    display: flex;
    font-size: 0.85rem;
    gap: 0.5rem;
    margin-top: 0.6rem;
    padding: 0.5rem 0.75rem;
}

.shortfall .icon {
    color: var(--warning);
    flex: none;
    margin-top: 0.1rem;
}

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

/* Cadence presets on the create form. Most runners don't know their steps per
   minute, so the common values are one tap away rather than something to guess. */
.preset-row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.5rem;
}

.preset {
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text-muted);
    cursor: pointer;
    font-family: inherit;
    font-size: 0.8rem;
    padding: 0.3rem 0.75rem;
    transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease;
}

.preset:hover {
    border-color: var(--accent);
    color: var(--text);
}

.preset.is-selected {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--text);
}

/* Landing hero photo. The scrim is a gradient layered in the same background
   shorthand as the image, so the hero's own text stays above it without any
   z-index juggling. It fades to --bg at the bottom so the panel dissolves into
   the page instead of ending on a hard edge. */
.hero-photo {
    background-color: var(--bg);
    background-image:
        linear-gradient(180deg,
            var(--bg) 0%,
            rgba(15, 17, 22, 0.52) 18%,
            rgba(15, 17, 22, 0.70) 62%,
            var(--bg) 100%),
        url("/img/hero-runners.jpg");
    /* The photo's dusk blue fights the coral accent, so blend it against the
       page background: the image keeps its luminance and takes its hue from
       --bg, leaving the CTA as the only saturated thing on screen. */
    background-blend-mode: normal, luminosity;
    background-size: cover;
    background-position: center 28%;
    background-repeat: no-repeat;
    /* Break out of the centred .container so the photo spans the window, like
       the header above it. The margins pull the section out to both edges; the
       padding keeps the copy off them. Paired with overflow-x: hidden on html,
       since 100vw counts a classic scrollbar and would otherwise overflow. */
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    padding-left: 1.5rem;
    padding-right: 1.5rem;
    padding-top: 6rem;
    padding-bottom: 6rem;
}

/* The section is full-width now, so cap the copy at a readable measure instead
   of letting the subtitle run the whole window. */
.hero-photo .title,
.hero-photo .subtitle {
    max-width: 46rem;
    margin-left: auto;
    margin-right: auto;
    /* Even out the line lengths so the copy doesn't break mid-phrase.
       Ignored by browsers that don't support it, which just wrap as before. */
    text-wrap: balance;
}

/* The muted greys these elements use elsewhere are for a flat --bg; over the
   photo they sit on luminance that varies from near-black to mid-grey. Lift
   them and give everything a soft shadow so the type holds up wherever the
   crop happens to land. */
.hero-photo .subtitle {
    color: var(--text);
}
.hero-photo .has-text-grey {
    color: rgba(255, 255, 255, 0.72) !important;
}
.hero-photo .hero-eyebrow,
.hero-photo .title,
.hero-photo .subtitle,
.hero-photo .has-text-grey {
    text-shadow: 0 1px 14px rgba(0, 0, 0, 0.6);
}

/* On phones the crop pushes the runners in behind the copy, so lean harder on
   the scrim and give the hero less height to fill. */
@media (max-width: 768px) {
    .hero-photo {
        background-image:
            linear-gradient(180deg,
                rgba(15, 17, 22, 0.55) 0%,
                rgba(15, 17, 22, 0.78) 60%,
                var(--bg) 100%),
            url("/img/hero-runners.jpg");
        padding-top: 3.5rem;
        padding-bottom: 3.5rem;
    }
}

/* Landing hero eyebrow — the line above the headline. Letter-spacing adds a
   trailing gap after the last letter, so nudge it back to stay optically centred. */
.hero-eyebrow {
    text-transform: uppercase;
    letter-spacing: 0.18em;
    text-indent: 0.18em;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--accent);
    margin-bottom: 1.25rem;
}

/* Landing "how it works" step number */
.step-number {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: 9999px;
    background-color: var(--accent);
    color: #fff;
    font-size: 1.25rem;
    font-weight: 700;
    margin-bottom: 0.75rem;
}

/* Admin usage charts */
.usage-chart {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: 120px;
}
.usage-bar {
    flex: 1 1 0;
    min-height: 2px;
    border-radius: 3px 3px 0 0;
    opacity: 0.85;
}
.usage-bar:hover {
    opacity: 1;
}
