/**
 * Inner pages: Projects, Lab, Music, About, Resume, Contact.
 * ==========================================================
 *
 * The home page was rebuilt on a small set of rules - an eyebrow above a
 * title, one hairline between sections, quiet cards, and three type faces
 * with one job each. This carries that to the other six pages. It is the
 * counterpart to home-refined.css (page bodies) and site-chrome.css (header,
 * footer, player), and it loads after both.
 *
 *
 * WHAT WAS WRONG
 *
 * Two things, on every page.
 *
 * First, boxes inside boxes. A page opened with a rounded, bordered, blurred
 * panel holding the title; beneath it another for the filters; then a grid of
 * cards with the same border and radius; and on Projects the breadcrumb above
 * all of them was a fourth. Four levels of one treatment, so none of them
 * meant anything - a border stops separating content from its surroundings
 * once the surroundings have the same border. The home page uses that
 * treatment once, on cards, and a hairline everywhere else.
 *
 * Second, five different mastheads. Projects used .page-header; Lab and Music
 * each carried an inline copy of the same 60px icon tile and an <h1> pinned
 * to "Albert Sans" 42px #826b7d; Resume and About had two more. All of them
 * put a Silkscreen line UNDER the title as a subtitle, where the home page
 * puts it above as an eyebrow. They are now one component - see
 * includes/components/page-head.php.
 *
 *
 * TYPE ROLES - the same three, unchanged from home-refined.css
 *
 *   Silkscreen   small caps labels only: eyebrows, meta, counts, tags. A
 *                pixel face reads as craft at 9-11px and as noise at
 *                paragraph size, so it never sets running text.
 *   ThaleahFat   display headings.
 *   Albert Sans  everything a person actually reads.
 *
 * No colours are introduced here. The ramp is the existing --re-brand-*, and
 * the hairline is --home-rule, the same one under the theme strip, between
 * home page sections and above the footer.
 */

/* ------------------------------------------------------------------ *
 * Visibility
 * ------------------------------------------------------------------ */

/*
 * The entrance animations are kept, and the reason they used to fail is fixed
 * at its source in assets/js/bs-init.js - AOS now re-measures whenever the
 * page height changes, instead of trusting one reading taken before the lazy
 * images had any height. See the note at the top of that file.
 *
 * There is deliberately no CSS override here forcing [data-aos] visible. One
 * existed briefly and it worked, but it worked by disabling the animations
 * altogether, which is a poor trade for something that can be fixed properly.
 *
 * If a page is ever reported blank again, the first thing to check is whether
 * AOS.refresh() is still being called on height changes; the second is
 * whether a new element carries data-aos inside a container that AOS cannot
 * measure, such as one that is display:none at init.
 */

/*
 * A print stylesheet must not depend on any of that, since none of the scroll
 * machinery runs for a print rendering.
 */
@media print {
    [data-aos] {
        opacity: 1 !important;
        transform: none !important;
    }
}

/* ------------------------------------------------------------------ *
 * Shared tokens
 * ------------------------------------------------------------------ */

.page-head,
.page-body {
    --page-rule: var(--home-rule, rgba(130, 107, 125, 0.22));
    --page-gap: clamp(24px, 3vw, 40px);
}

/* ------------------------------------------------------------------ *
 * Masthead
 * ------------------------------------------------------------------ */

.page-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 24px;
    align-items: end;
    padding: clamp(20px, 3vw, 40px) 0 clamp(18px, 2.4vw, 28px);
    margin-bottom: clamp(28px, 4vw, 48px);
    border-bottom: 1px solid var(--page-rule);
}

.page-head-eyebrow {
    display: block;
    font-family: var(--re-font-ui);
    font-size: 0.64rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.5;
    margin-bottom: 12px;
}

/*
 * The !important is against theme-color-fix.css, which sets the display face
 * on h1..h6 with !important of its own. That is right for headings generally;
 * it just cannot be argued with from a class selector without matching it.
 * The size is the argument here, not the face.
 */
.page-head-title {
    font-family: var(--re-font-display) !important;
    font-size: clamp(2.1rem, 5vw, 3.4rem);
    line-height: 1;
    letter-spacing: 0.01em;
    margin: 0;
    color: var(--re-text-color);
    /* The reactive background moves behind this, so the type needs its own
       weight against a field that is not still. */
    text-shadow: 0 2px 20px rgba(0, 0, 0, 0.5);
}

.page-head-lead {
    font-family: var(--re-font-body);
    font-size: clamp(0.98rem, 1.3vw, 1.12rem);
    line-height: 1.55;
    color: var(--re-text-color);
    opacity: 0.72;
    max-width: 62ch;
    margin: 16px 0 0;
}

.page-head-side {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 14px;
    padding-bottom: 4px;
}

.page-head-meta {
    display: flex;
    align-items: center;
    gap: 10px;
    font-family: var(--re-font-ui);
    font-size: 0.6rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.5;
    white-space: nowrap;
}

.page-head-meta-dot {
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background: currentColor;
    opacity: 0.7;
}

.page-head-actions {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* ------------------------------------------------------------------ *
 * Sections
 * ------------------------------------------------------------------ */

.page-section {
    margin-bottom: clamp(40px, 6vw, 72px);
}

.page-section-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 20px;
    flex-wrap: wrap;
    margin-bottom: var(--page-gap);
    padding-bottom: 14px;
    border-bottom: 1px solid var(--page-rule);
}

.page-section-title {
    font-family: var(--re-font-display) !important;
    font-size: clamp(1.3rem, 2.2vw, 1.75rem);
    line-height: 1.1;
    margin: 0;
    color: var(--re-text-color);
}

.page-section-note {
    font-family: var(--re-font-ui);
    font-size: 0.6rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.45;
}

/* ------------------------------------------------------------------ *
 * De-carding
 * ------------------------------------------------------------------ */

/*
 * The panels that used to hold each page's title. The markup is gone from the
 * templates, but these three classes are still referenced by other pages and
 * by page-standard.css, so they are neutralised rather than deleted: any that
 * survive somewhere become a plain block instead of a fifth nested card.
 */
.page-header,
.lab-header,
.music-header {
    background: none !important;
    border: none !important;
    border-radius: 0 !important;
    box-shadow: none !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
    padding: 0 !important;
}

/*
 * Filter and control strips. These sat in their own bordered, blurred panel
 * directly beneath the title panel - two cards before a visitor reached a
 * single project. A row of controls under a hairline reads as a toolbar for
 * the list below it, which is what it is.
 */
.filter-bar,
.page-toolbar {
    background: none !important;
    border: none !important;
    border-top: 1px solid var(--home-rule, rgba(130, 107, 125, 0.22)) !important;
    border-radius: 0 !important;
    box-shadow: none !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
    padding: 20px 0 0 !important;
    margin-bottom: clamp(28px, 4vw, 44px) !important;
}

/*
 * The breadcrumb. On an inner page it sat above the title as a full-width
 * bordered pill - a card whose entire content is "HOME / PROJECTS". It keeps
 * its place in the reading order and loses the box.
 */
.breadcrumb {
    background: none !important;
    border: none !important;
    border-radius: 0 !important;
    box-shadow: none !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
    padding: 0 !important;
    margin-bottom: 0 !important;
}

/*
 * THE PSEUDO-ELEMENTS HAVE TO GO WITH THE BOX.
 *
 * Clearing background, border and radius is only half of removing a glass
 * panel. glass-standard.css also gives each of these a ::before holding a
 * full-bleed noise texture and an ::after drawing a bright 1px highlight
 * across the top edge, and breadcrumb-standard.css adds its own pair. Those
 * kept painting after the box beneath them was gone, so what was left was a
 * faint hard-cornered rectangle of noise with a lit strip along its top,
 * attached to nothing. It read as a rendering artifact because that is
 * effectively what it was.
 *
 * The noise inherits border-radius, which made it worse: with the radius
 * forced to 0 the ghost had square corners while everything near it was
 * round.
 *
 * Anything de-carded above has to be listed here too.
 */
.breadcrumb::before,
.breadcrumb::after,
.page-header::before,
.page-header::after,
.lab-header::before,
.lab-header::after,
.music-header::before,
.music-header::after,
.filter-bar::before,
.filter-bar::after,
body[data-page="portfolio"] .container.glass::before,
body[data-page="portfolio"] .container.glass::after {
    content: none !important;
}

/*
 * The trail also sits above a gradient rule drawn by breadcrumb.php, which
 * lands as a second hairline immediately above the masthead's own. One line
 * is enough.
 */
.breadcrumb + .article-separator {
    display: none;
}

/* ------------------------------------------------------------------ *
 * Controls
 * ------------------------------------------------------------------ */

.filter-btn,
.page-chip {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 8px 14px;
    border: 1px solid var(--page-rule);
    border-radius: var(--re-radius-pill, 999px);
    background: transparent;
    color: var(--re-text-color);
    font-family: var(--re-font-ui);
    font-size: 0.6rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    text-decoration: none;
    opacity: 0.72;
    transition: opacity 0.16s ease, background 0.16s ease, border-color 0.16s ease;
}

.filter-btn:hover,
.page-chip:hover {
    opacity: 1;
    color: var(--re-text-color);
    background: rgba(var(--re-brand-400-rgb), 0.14);
}

.filter-btn.active,
.page-chip.active {
    opacity: 1;
    color: var(--re-text-color);
    background: rgba(var(--re-brand-400-rgb), 0.24);
    border-color: var(--re-brand-400);
}

.view-toggle-btn {
    width: 34px;
    height: 34px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid var(--page-rule);
    border-radius: var(--re-radius-sm, 6px);
    background: transparent;
    color: var(--re-text-color);
    opacity: 0.6;
    cursor: pointer;
    transition: opacity 0.16s ease, background 0.16s ease;
}

.view-toggle-btn:hover { opacity: 1; }

.view-toggle-btn.active {
    opacity: 1;
    background: rgba(var(--re-brand-400-rgb), 0.22);
    border-color: var(--re-brand-400);
}

/*
 * The solid variant, for the one action on a page that is the point of it -
 * Download on the resume.
 */
.page-chip-solid {
    background: rgba(var(--re-brand-400-rgb), 0.26);
    border-color: var(--re-brand-400);
    opacity: 1;
}

.page-chip-solid:hover {
    background: rgba(var(--re-brand-400-rgb), 0.4);
    border-color: var(--re-brand-200);
}

/* ------------------------------------------------------------------ *
 * Resume
 * ------------------------------------------------------------------ */

/*
 * Email, phone, website, LinkedIn, GitHub. Reference information, so it sits
 * quietly under a hairline rather than as five buttons in the masthead.
 */
.resume-contact {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px 24px;
    padding-bottom: 22px;
    margin-bottom: clamp(32px, 5vw, 56px);
    border-bottom: 1px solid var(--page-rule);
    font-family: var(--re-font-body);
    font-size: 0.88rem;
}

.resume-contact a,
.resume-contact span {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--re-text-color);
    opacity: 0.66;
    text-decoration: none;
    transition: opacity 0.16s ease;
}

.resume-contact a:hover {
    opacity: 1;
    color: var(--re-text-color);
}

.resume-contact i {
    color: var(--re-brand-300);
    font-size: 0.95em;
}

/* The dropdown wrapper is a span in a flex row; it must not inherit the
   muted treatment meant for the plain items beside it. */
.resume-contact .dropdown {
    opacity: 1;
}

/* ------------------------------------------------------------------ *
 * About
 * ------------------------------------------------------------------ */

/*
 * About keeps its own opening rather than being given a masthead, because it
 * already has one: a portrait, a name and an intro, which is the same shape
 * as the home page hero. What it did not have was the site's order or its
 * type - so the panel goes, the subtitle moves above the title as an eyebrow,
 * and the title comes up to masthead scale.
 */
.about-hero,
.about-hero-container {
    background: none !important;
    border: none !important;
    border-radius: 0 !important;
    box-shadow: none !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
    padding: 0 !important;
}

.about-hero {
    padding-bottom: clamp(28px, 4vw, 44px) !important;
    border-bottom: 1px solid var(--home-rule, rgba(130, 107, 125, 0.22)) !important;
}

/*
 * column-reverse rather than reordered markup: the subtitle follows the title
 * in the HTML, which is the correct reading order for a screen reader and for
 * a page with no CSS. Only the painted order changes.
 */
.about-hero .intro-title,
.about-hero .intro-subtitle {
    margin: 0;
}

.intro-title {
    font-family: var(--re-font-display) !important;
    font-size: clamp(2rem, 4.4vw, 3rem) !important;
    line-height: 1 !important;
    color: var(--re-text-color) !important;
    order: 2;
}

.intro-subtitle {
    font-family: var(--re-font-ui) !important;
    font-size: 0.64rem !important;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--re-text-color) !important;
    opacity: 0.5;
    order: 1;
    margin-bottom: 12px !important;
}

.intro-content {
    font-family: var(--re-font-body);
    font-size: clamp(0.98rem, 1.3vw, 1.1rem);
    line-height: 1.65;
    color: var(--re-text-color);
    opacity: 0.75;
    order: 3;
    margin-top: 18px !important;
    max-width: 62ch;
}

/* The intro column is the flex context those orders apply in. */
.about-hero .col-md-8,
.about-hero .col-md-7 {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
}

/*
 * Section headings on About and on the gaming carousels. Same inversion: the
 * .section-meta line was a subtitle beneath the title and becomes the eyebrow
 * above it.
 */
.section-header-accent {
    border: none !important;
    background: none !important;
    padding: 0 0 14px !important;
    margin-bottom: var(--page-gap, 32px) !important;
    border-bottom: 1px solid var(--home-rule, rgba(130, 107, 125, 0.22)) !important;
}

.section-header-text {
    display: flex;
    flex-direction: column;
}

.section-header-text .section-title {
    font-family: var(--re-font-display) !important;
    font-size: clamp(1.3rem, 2.2vw, 1.75rem) !important;
    line-height: 1.1 !important;
    margin: 0 !important;
    color: var(--re-text-color) !important;
    order: 2;
}

.section-header-text .section-meta,
.section-header-text .section-subtitle {
    font-family: var(--re-font-ui) !important;
    font-size: 0.6rem !important;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--re-text-color) !important;
    opacity: 0.45;
    order: 1;
    margin: 0 0 10px !important;
}

.section-header-text .section-title i {
    color: var(--re-brand-300);
    font-size: 0.8em;
}

/*
 * The content sections further down About - "Who I Am", "Skills &
 * Technologies", the experience list. These are not wrapped in
 * .section-header-text: the title and subtitle are plain siblings inside
 * [data-about-section], so they need their own inversion.
 *
 * Everything gets order 3 first, then the two headings are pulled in front of
 * it. Without that base, any child this file does not name would default to
 * order 0 and jump above the heading - a new section type added later would
 * render its body before its title.
 */
[data-about-section] {
    display: flex;
    flex-direction: column;
    align-items: stretch;
}

[data-about-section] > * {
    order: 3;
}

[data-about-section] > .section-subtitle {
    order: 1;
    font-family: var(--re-font-ui) !important;
    font-size: 0.6rem !important;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--re-text-color) !important;
    opacity: 0.45;
    margin: 0 0 10px !important;
}

[data-about-section] > .section-title {
    order: 2;
    font-family: var(--re-font-display) !important;
    font-size: clamp(1.3rem, 2.2vw, 1.75rem) !important;
    line-height: 1.1 !important;
    color: var(--re-text-color) !important;
    margin: 0 0 var(--page-gap, 32px) !important;
    padding-bottom: 14px !important;
    border-bottom: 1px solid var(--home-rule, rgba(130, 107, 125, 0.22));
}

[data-about-section] > .section-content {
    font-family: var(--re-font-body);
    font-size: 1rem;
    line-height: 1.7;
    color: var(--re-text-color);
    opacity: 0.78;
    max-width: 68ch;
}

/*
 * The sections keep their panels.
 *
 * These were stripped to bare text on a hairline, on the same reasoning that
 * removed the panels elsewhere, and it was wrong here. On Projects or
 * Portfolio the panel wrapped a grid of cards that already had borders of
 * their own, so it was a box around boxes. On About each section is a block
 * of running prose, and the panel is the only thing giving it an edge -
 * without one the page is an undifferentiated column of text, and the rounded
 * corner that belonged to the design goes with it.
 *
 * What does change is that "contained" and "open" no longer differ. They were
 * two surfaces at different opacities alternating down the page, so every
 * other section read as a different kind of thing. One treatment for both.
 */
.about-section,
.about-section.contained,
.about-section.open,
.gaming-section {
    background: var(--re-surface) !important;
    backdrop-filter: var(--re-glass);
    -webkit-backdrop-filter: var(--re-glass);
    -webkit-backdrop-filter: blur(16px) saturate(170%) !important;
    backdrop-filter: blur(16px) saturate(170%) !important;
    border: 1px solid var(--re-border) !important;
    border-radius: var(--re-radius-lg) !important;
    box-shadow: 0 6px 28px rgba(0, 0, 0, 0.26) !important;
    padding: clamp(24px, 3.4vw, 40px) !important;
}

/* Inside a panel the heading rule is an underline for the heading, not a line
   cutting the panel in half, so it takes the panel's own border colour. */
[data-about-section] > .section-title {
    border-bottom-color: var(--re-border);
}

/* ------------------------------------------------------------------ *
 * Gaming carousels
 * ------------------------------------------------------------------ */

/*
 * about-page.css fades the cards out at both ends of the carousel, under the
 * arrow gutters, with two 48px gradients running to --re-ground-outer.
 *
 * A fade like that works only when it sits on a surface of exactly that
 * colour, and this one does not: the panel behind it is translucent glass
 * over the page's moving background. So the gradients faded to a flat colour
 * matching nothing behind them - two grey smears standing 48px in from each
 * edge, each with a visible vertical seam where it began.
 *
 * A fade has to know the colour behind it. This one cannot, so it goes. The
 * cards now run to the edge of the scroller, which is at least honest about
 * there being more of them.
 */
.games-carousel-container::before,
.games-carousel-container::after {
    content: none !important;
}

/* ------------------------------------------------------------------ *
 * Contact
 * ------------------------------------------------------------------ */

/*
 * Portfolio and Contact both wrapped their whole body in a full-width glass
 * panel that also held the page heading. The heading is the masthead now, and
 * the two pages part company here.
 *
 * Portfolio is a grid of cards carrying their own borders, so its panel was a
 * box drawn around boxes and it goes entirely.
 *
 * Contact is a single form, which needs an edge to sit against - the fields
 * are the content and they do not frame themselves. Its panel stays, with the
 * rounded corner it always had. Only the heading has left it.
 */
body[data-page="portfolio"] .container.glass {
    background: none !important;
    border: none !important;
    border-radius: 0 !important;
    box-shadow: none !important;
    -webkit-backdrop-filter: none !important;
    backdrop-filter: none !important;
    padding-left: 0 !important;
    padding-right: 0 !important;
}

body[data-page="contact"] .container.glass {
    border-radius: var(--re-radius-lg) !important;
}

/* ------------------------------------------------------------------ *
 * Narrow screens
 * ------------------------------------------------------------------ */

@media (max-width: 768px) {
    /*
     * A gutter. The home page has one; the inner pages ran their content to
     * the very edge of the screen because the breadcrumb container sets its
     * side padding to 0 inline, and nothing put it back.
     */
    body:not([data-page="home"]) .container.effect {
        padding-left: 16px !important;
        padding-right: 16px !important;
    }

    /*
     * Sideways scroll on Music.
     *
     * The page is a two-column grid, "280px 1fr", collapsed to a single 1fr
     * column on a phone by a rule in the template. But a 1fr track is
     * min-width:auto by default, so it will not shrink below its widest
     * child - and the widest child is a track title like "Super Street
     * Fighter II Turbo - Guiles Stage". The column stayed 394px wide inside a
     * 375px screen and took the whole document with it: the navbar, the
     * player bar and the footer all measured 409.
     *
     * min-width: 0 lets the track shrink; the ellipsis rules below give the
     * text somewhere to go when it does.
     */
    body[data-page="music"] [style*="grid-template-columns"] > * {
        min-width: 0;
    }

    /*
     * The track row holds five things: index, art, title/artist, a genre
     * badge and a duration. At 375px that leaves the title about 33px, so
     * "Super Street Fighter II Turbo - Guiles Stage" arrives as "Super…".
     *
     * The index and the badge are the two that carry least: the row order is
     * already visible, and the genre is a filter in the sidebar above. Drop
     * those, halve the art, tighten the padding, and the title gets roughly
     * 200px - enough to be a title rather than a hint of one.
     */
    .track-item {
        min-width: 0;
        gap: 12px !important;
        padding: 12px 4px !important;
    }

    .track-index,
    .track-genre {
        display: none !important;
    }

    .track-art {
        width: 44px !important;
        height: 44px !important;
        flex: 0 0 auto;
    }

    .track-meta {
        min-width: 0;
    }

    .track-duration {
        min-width: 40px !important;
        flex: 0 0 auto;
    }

    .page-head {
        grid-template-columns: minmax(0, 1fr);
        gap: 16px;
        align-items: start;
    }

    /*
     * Counts and controls move under the title rather than being squeezed
     * into a second column beside a heading set at 5vw.
     */
    .page-head-side {
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        width: 100%;
        padding-bottom: 0;
    }

    .page-head-lead {
        font-size: 0.95rem;
    }

    .page-section-head {
        gap: 8px;
    }
}

@media (prefers-reduced-motion: reduce) {
    .filter-btn,
    .page-chip,
    .view-toggle-btn {
        transition: none;
    }
}

/* ------------------------------------------------------------------ *
 * Platform badges
 * ------------------------------------------------------------------ */

/*
 * The consoles a project shipped on, as their own wordmarks. See
 * includes/components/platform-badges.php.
 *
 * Sized by HEIGHT and never by width. The logos are single-colour white SVGs
 * with very different aspect ratios - Xbox Series is 3839x1575, Xbox 360 is
 * 3786x784 - so a fixed width would render one of them tiny and the other
 * enormous. A common height is what makes a row of different wordmarks read as
 * one set.
 */
.platform-badges {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px 18px;
    list-style: none;
    margin: 14px 0 0;
    padding: 0;
}

.platform-badge {
    display: inline-flex;
    align-items: center;
}

.platform-badge img {
    height: 15px;
    width: auto;
    max-width: 150px;
    display: block;
    /* The wordmarks are pure white; at full strength they out-shout the
       headings above them, so they sit back until hovered. */
    opacity: 0.62;
    transition: opacity 0.18s ease;
}

.platform-badges:hover .platform-badge img,
.platform-badge img:hover {
    opacity: 0.95;
}

/*
 * A platform with no logo yet. Shown rather than dropped: omitting it would
 * quietly claim the project did not ship there.
 */
.platform-badge-text {
    font-family: var(--re-font-ui);
    font-size: 0.56rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.5;
    padding: 3px 9px;
    border: 1px solid var(--home-rule, rgba(130, 107, 125, 0.22));
    border-radius: var(--re-radius-pill, 999px);
}

/* On a card the row is one line of small marks, not a feature in itself. */
.home-card .platform-badges,
.portfolio-card .platform-badges {
    gap: 8px 12px;
    margin-top: 10px;
}

.home-card .platform-badge img,
.portfolio-card .platform-badge img {
    height: 12px;
    max-width: 110px;
}

@media (max-width: 768px) {
    .platform-badge img { height: 13px; max-width: 120px; }
}

@media (prefers-reduced-motion: reduce) {
    .platform-badge img { transition: none; }
}

/* ------------------------------------------------------------------ *
 * Portfolio grid
 * ------------------------------------------------------------------ */

/*
 * Replaces a Bootstrap .row of .card inside a .container.glass.
 *
 * The problems it fixes, in the order they were visible:
 *
 *   Cards ended wherever their text ended, so three in a row finished at three
 *   different heights and no two buttons lined up. Each card is a flex column
 *   now and .pf-actions carries margin-top:auto, which pushes the buttons to
 *   the bottom of whatever space the card has.
 *
 *   The platform wordmarks sat flush against the buttons - 12px marks with a
 *   button row that had no top margin - so they read as overlapping them.
 *   .pf-platforms has its own space above and below.
 *
 *   And the whole grid sat inside a glass panel, which was a box drawn around
 *   a set of boxes. Gone; the cards are the only surface.
 */

.pf-grid {
    display: grid;
    /* auto-fill rather than a fixed column count: the grid decides how many
       fit, so it works at every width without breakpoints. */
    grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
    gap: clamp(20px, 2.4vw, 32px);
    list-style: none;
    margin: 0 0 clamp(48px, 7vw, 90px);
    padding: 0;
}

/*
 * The portfolio card.
 *
 * This was a flat panel: a hardcoded plum at 50% and no blur, while the
 * project cards next door used the site's standard recipe and had both. Two
 * grids of the same thing, one of them glass and one of them not.
 *
 * It is the standard recipe now - the same three tokens .standard-card uses
 * in page-standard.css - so the two agree, and the card follows the palette
 * instead of staying purple under every theme.
 */
.pf-card {
    display: flex;
    flex-direction: column;
    background: var(--re-surface);
    backdrop-filter: var(--re-glass);
    -webkit-backdrop-filter: var(--re-glass);
    border: 1px solid var(--re-border);
    border-radius: var(--re-radius-lg, 14px);
    overflow: hidden;
    transition: border-color 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
}

.pf-card:hover {
    border-color: var(--re-border-strong);
    transform: translateY(-3px);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}

/* --- media -------------------------------------------------------- */

.pf-media {
    display: block;
    /* A ratio rather than a fixed height: every card's image is the same
       shape, and the shape holds as the column width changes. */
    aspect-ratio: 16 / 10;
    background: rgba(var(--re-brand-400-rgb), 0.12);
    overflow: hidden;
}

.pf-media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform 0.4s ease;
}

.pf-card:hover .pf-media img { transform: scale(1.03); }

.pf-media-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    font-family: var(--re-font-ui);
    font-size: 0.56rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.35;
}

/* --- body --------------------------------------------------------- */

.pf-body {
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding: 18px 18px 20px;
    flex: 1 1 auto;
}

.pf-title {
    font-family: var(--re-font-display) !important;
    font-size: 1.05rem;
    line-height: 1.15;
    margin: 0;
    color: var(--re-text-color);
}

.pf-title a { color: inherit; text-decoration: none; }
.pf-title a:hover { color: var(--re-brand-100); }

.pf-desc {
    font-family: var(--re-font-body);
    font-size: 0.88rem;
    line-height: 1.5;
    color: var(--re-text-color);
    opacity: 0.72;
    margin: 0;
    /* Clamped rather than truncated in PHP: the whole description stays in the
       DOM for search and for a screen reader, and no ellipsis is added to text
       that did not need one. */
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.pf-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    list-style: none;
    margin: 0;
    padding: 0;
}

.pf-tags li {
    font-family: var(--re-font-ui);
    /* Silkscreen has no '#', so C# renders as a missing glyph at label sizes.
       Body face for anything that can contain punctuation. */
    font-size: 0.6rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.6;
    padding: 3px 8px;
    border: 1px solid var(--re-border);
    border-radius: var(--re-radius-pill, 999px);
}

/* --- platforms ---------------------------------------------------- */

/*
 * Its own band, with a hairline above. The marks are small and white; without
 * separation they read as part of whatever is next to them - which is exactly
 * how they ended up looking like they were on top of the buttons.
 */
.pf-platforms {
    padding-top: 12px;
    border-top: 1px solid var(--re-border-faint);
}

.pf-platforms .platform-badges { margin: 0; gap: 8px 14px; }
.pf-platforms .platform-badge img { height: 13px; max-width: 96px; opacity: 0.7; }
.pf-platforms .platform-badge img:hover { opacity: 1; }

/* --- actions ------------------------------------------------------ */

/*
 * margin-top:auto is the whole trick: it takes the free space in the card and
 * puts it above this row, so every card in a row ends level however long its
 * description happens to be.
 */
.pf-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: auto;
    padding-top: 14px;
}

.pf-btn {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 8px 14px;
    border-radius: var(--re-radius-sm, 6px);
    border: 1px solid var(--re-border-strong);
    background: rgba(93, 75, 96, 0.35);
    color: var(--re-text-color);
    font-family: var(--re-font-ui);
    font-size: 0.58rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    text-decoration: none;
    transition: background 0.16s ease, border-color 0.16s ease;
}

.pf-btn:hover {
    background: rgba(var(--re-brand-400-rgb), 0.4);
    border-color: var(--re-brand-200);
    color: var(--re-text-color);
}

.pf-btn-primary {
    background: rgba(var(--re-brand-400-rgb), 0.42);
    border-color: var(--re-brand-400);
}

.pf-btn i { font-size: 0.85em; opacity: 0.8; }

/* --- empty state -------------------------------------------------- */

.pf-empty {
    text-align: center;
    padding: 60px 20px 90px;
    color: var(--re-text-color);
}

.pf-empty i { font-size: 48px; opacity: 0.3; display: block; margin-bottom: 14px; }
.pf-empty p { font-family: var(--re-font-body); opacity: 0.6; margin: 0; }

@media (max-width: 480px) {
    .pf-grid { grid-template-columns: minmax(0, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
    .pf-card,
    .pf-media img { transition: none; }
    .pf-card:hover { transform: none; }
    .pf-card:hover .pf-media img { transform: none; }
}

/* ------------------------------------------------------------------ *
 * Music
 * ------------------------------------------------------------------ */

/*
 * Replaces a 280px sidebar + `1fr` grid, three glass panels and about sixty
 * inline styles.
 *
 * The bug that made this urgent: the track list rendered ~315px wider than the
 * page, past the masthead rule and off the right edge. `1fr` is shorthand for
 * `minmax(auto, 1fr)`, and that `auto` minimum is the column's MIN-CONTENT -
 * which the widest row put at 1150px against 836px of available space, so the
 * column simply refused to be narrower and overflowed the grid.
 *
 * The inline `min-width: 0` on the track text did not help, and that is worth
 * understanding rather than working around: min-width on a flex ITEM governs
 * how far flex may shrink it, not the min-content size its block parent
 * reports upward to the grid. The grid asked the column, not the flex item.
 *
 * So the filters are a row across the top now rather than a column beside the
 * list, and the rows are a CSS grid whose text track is an explicit
 * `minmax(0, 1fr)` - a real zero minimum, which cannot be talked out of it by
 * anything inside. The list is full width, and there is no second column left
 * to blow out.
 */

/*
 * .pg-* rather than .mu-*: Projects, Lab and Music all filter a listing by a
 * handful of short labels, and there is no reason for that control to look
 * like three different things. Lab drew its filters as a glass panel of pill
 * buttons with inline onmouseover handlers; Projects drew them as .filter-btn
 * from projects-page.css. Both are this now.
 */
.pg-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 10px;
    align-items: center;
    margin: 0 0 10px;
    padding: 0;
    list-style: none;
}

.pg-filters + .pg-filters {
    margin-top: 2px;
}

.pg-filter-label {
    font-family: var(--re-font-ui);
    font-size: 0.56rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.4;
    margin-right: 6px;
    /* Keeps the label from being pulled taller than the pills beside it. */
    align-self: center;
}

.pg-chip {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 6px 12px;
    border: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
    border-radius: 999px;
    background: transparent;
    color: var(--re-text-color);
    text-decoration: none;
    font-family: var(--re-font-ui);
    font-size: 0.56rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    opacity: 0.72;
    transition: border-color 0.16s ease, background 0.16s ease, opacity 0.16s ease;
    white-space: nowrap;
}

.pg-chip:hover {
    opacity: 1;
    border-color: var(--re-border-strong);
    background: var(--re-surface-raised);
    color: var(--re-text-color);
}

.pg-chip[aria-current="true"] {
    opacity: 1;
    border-color: rgba(186, 158, 187, 0.55);
    background: rgba(186, 158, 187, 0.16);
}

/* The count rides inside the pill, dimmer than the name. */
/*
 * The count inside a filter chip.
 *
 * It was 0.92em, which against the chip's 8px came out at 7.36px - Silkscreen
 * resampled to 92% of the size it is drawn at, which is exactly the blur the
 * pixel grid is meant to prevent. It reads as smaller than the label anyway,
 * because it is dimmed to half opacity.
 *
 * The em is the trap: this element inherits the UI face rather than asking
 * for it, so silkscreen-pixel-grid.css never sees it and cannot correct it.
 * Anything inheriting Silkscreen has to state 8px outright.
 */
.pg-chip-n {
    font-size: var(--re-size-ui, 8px);
    opacity: 0.5;
    font-variant-numeric: tabular-nums;
}

/* --- grouped ------------------------------------------------------- */

/*
 * A group heading already draws a rule under itself (.page-section-head has a
 * bottom border), and the list draws one over itself. Together they made a
 * double line under every heading.
 */
/*
 * Under a "CPS-2" heading, every row saying CPS-2 is noise - the heading has
 * already said it. The badge stays when grouping by album, where the system is
 * the one thing the heading does not tell you.
 */
.mu-group-system .mu-system {
    display: none;
}

.mu-group .mu-list {
    border-top: none;
}

/* Tighter than a full page section: these are headings within one list, not
   separate parts of the page. */
.mu-group {
    margin-bottom: clamp(28px, 4vw, 48px);
}

.mu-group .page-section-head {
    margin-bottom: 0;
    padding-bottom: 10px;
}

/* --- the list ------------------------------------------------------ */

.mu-list {
    list-style: none;
    margin: 0;
    padding: 0;
    border-top: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
}

/*
 * The row. Five explicit tracks; the third is the only flexible one and its
 * minimum is a literal 0 - see the note at the top of this section for why
 * that is the whole fix.
 */
.mu-track {
    display: grid;
    /* index, art, text, system, duration, playlist button */
    grid-template-columns: 30px 52px minmax(0, 1fr) auto auto auto;
    align-items: center;
    gap: 16px;
    padding: 11px 10px;
    border-bottom: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
    cursor: pointer;
    transition: background 0.16s ease;
}

.mu-track:hover,
.mu-track:focus-visible {
    background: var(--re-surface-raised);
    outline: none;
}

.mu-track.is-playing {
    background: rgba(186, 158, 187, 0.1);
}

/* --- index / play --------------------------------------------------- */

.mu-index {
    position: relative;
    text-align: center;
    font-family: var(--re-font-ui);
    font-size: 0.58rem;
    color: var(--re-text-color);
    opacity: 0.4;
    font-variant-numeric: tabular-nums;
}

/*
 * Number and play icon are stacked rather than swapped with display, so the
 * cell never changes width on hover and the row does not twitch.
 */
.mu-index i {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.1rem;
    color: var(--re-brand-200, #ba9ebb);
    opacity: 0;
    transition: opacity 0.16s ease;
}

.mu-track:hover .mu-index span { opacity: 0; }
.mu-track:hover .mu-index i    { opacity: 1; }
.mu-track:hover .mu-index      { opacity: 1; }

/* --- art ------------------------------------------------------------ */

.mu-art {
    width: 52px;
    height: 52px;
    border-radius: var(--re-radius-sm, 6px);
    object-fit: cover;
    background: var(--re-surface-raised);
    border: 1px solid var(--re-border);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--re-brand-300);
    font-size: 1.1rem;
}

/* --- text ----------------------------------------------------------- */

/*
 * min-width: 0 again here. It is doing a different job from the grid track:
 * this is what lets the ellipsis happen, by allowing the block to be narrower
 * than the nowrap line inside it.
 */
.mu-text {
    min-width: 0;
}

.mu-title {
    font-family: var(--re-font-body);
    font-size: 0.94rem;
    font-weight: 600;
    color: var(--re-text-color);
    margin: 0 0 3px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.mu-by {
    font-family: var(--re-font-body);
    font-size: 0.8rem;
    color: var(--re-text-color);
    opacity: 0.55;
    margin: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --- system badge --------------------------------------------------- */

/*
 * Always rendered, even with nothing in it. The row is a grid with a fixed
 * number of tracks, so a row that skipped this cell pushed its duration
 * onto a second line - which is what happened on mobile, where the artwork
 * cell is hidden and the counts stopped lining up. :empty takes the pill
 * off so an empty cell is invisible rather than a stray outline.
 */
.mu-system:empty {
    border-color: transparent;
    padding: 0;
}

.mu-system {
    font-family: var(--re-font-ui);
    font-size: 0.5rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.55;
    padding: 4px 9px;
    border: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
    border-radius: 999px;
    white-space: nowrap;
}

.mu-duration {
    font-family: var(--re-font-ui);
    font-size: 0.56rem;
    color: var(--re-text-color);
    opacity: 0.4;
    font-variant-numeric: tabular-nums;
    text-align: right;
    min-width: 34px;
}

/* --- empty ---------------------------------------------------------- */

.pg-empty {
    text-align: center;
    padding: clamp(48px, 9vw, 90px) 20px;
    border: 1px dashed var(--page-rule, rgba(130, 107, 125, 0.22));
    border-radius: var(--re-radius-lg, 14px);
    color: var(--re-text-color);
}

.pg-empty i {
    font-size: 2.4rem;
    opacity: 0.35;
    display: block;
    margin-bottom: 14px;
}

.pg-empty p {
    font-family: var(--re-font-body);
    opacity: 0.6;
    margin: 0 0 20px;
}

/* --- narrow --------------------------------------------------------- */

@media (max-width: 720px) {
    /*
     * Art and the system badge go. On a 375px screen the badge alone was
     * taking a third of the row, and the cover art on these is a stock note
     * glyph for all but a handful of tracks - it is not carrying information
     * worth the width.
     */
    .mu-track {
        /* index, text, duration, playlist button */
        grid-template-columns: 26px minmax(0, 1fr) auto auto;
        gap: 12px;
        padding: 10px 4px;
    }

    /* The controls survive the narrow layout; the badge and artwork do not. */
    .mu-track.has-admin {
        grid-template-columns: 26px minmax(0, 1fr) auto auto auto;
    }

    .mu-admin { gap: 2px; }

    .mu-art,
    .mu-system {
        display: none;
    }

    .mu-title { font-size: 0.9rem; }
    .mu-by    { font-size: 0.75rem; }
}

/* ------------------------------------------------------------------ *
 * About
 * ------------------------------------------------------------------ */

/*
 * The last bespoke page shell on the site.
 *
 * About had its own hero - a Bootstrap .row with the avatar in a col-md-4 and
 * an <h1> in a col-md-8 - which is not what any other page does, so the page
 * announced itself in a different voice from Home, Music and the rest. It also
 * alternated its sections between a "contained" panel and an "open" one, which
 * read as two different designs interleaved rather than one rhythm.
 *
 * It now opens with the shared .page-head like every other inner page, and
 * every section below is a .page-section with a .page-section-head - the same
 * title-plus-hairline the Music page uses. What is left here is only the parts
 * About genuinely has and no other page does: the profile row, the skill
 * meters, the experience list and the game shelves.
 *
 * New .ab-* names on purpose. about-page.css still loads (21st of 32) and
 * carries the old .profile-*, .section-* and .skills-grid rules; reusing those
 * names would have meant winning a specificity argument on every declaration.
 * A new namespace simply does not have the argument.
 */

/* --- identity block -------------------------------------------------- */

/*
 * Who is writing, and what they do, side by side.
 *
 * This was a full-width row holding only the avatar, the name and five icons -
 * 141px of page carrying about a line of information - with the paragraph that
 * belonged next to it stranded in its own section further down. The two halves
 * are one grid now, which is both shorter and the correct pairing.
 *
 * The bio column is the wider one: it is the part with something to say.
 */
.ab-identity {
    display: grid;
    grid-template-columns: minmax(220px, 20rem) minmax(0, 1fr);
    align-items: start;
    gap: clamp(24px, 4vw, 56px);
    padding: 0 0 clamp(24px, 3.5vw, 36px);
    margin-bottom: clamp(28px, 4vw, 44px);
    border-bottom: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
}

/* No bio in the database - the left column takes the full width rather than
   leaving a hole where the paragraph would have been. */
.ab-identity.is-alone {
    grid-template-columns: minmax(0, 1fr);
}

.ab-who {
    display: flex;
    align-items: center;
    /* Wraps so the links get a full-width row under the avatar and the name
       rather than the strip left over beside them. */
    flex-wrap: wrap;
    gap: clamp(14px, 2vw, 20px);
}

/* min-width: 0 so a long tagline ellipsises instead of widening the column. */
.ab-who-text {
    min-width: 0;
}

.ab-bio {
    /* Optically level with the name rather than the top of the avatar. */
    padding-top: 4px;
}

.ab-avatar {
    width: clamp(76px, 9vw, 104px);
    height: clamp(76px, 9vw, 104px);
    border-radius: 50%;
    object-fit: cover;
    flex: 0 0 auto;
    border: 1px solid var(--re-border-strong);
    background: var(--re-surface-raised);
}

.ab-profile-name {
    font-family: var(--re-font-display) !important;
    font-size: clamp(1.1rem, 1.9vw, 1.4rem);
    line-height: 1.1;
    margin: 0 0 6px;
    color: var(--re-text-color);
}

.ab-profile-tagline {
    font-family: var(--re-font-ui);
    font-size: 0.56rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.45;
    margin: 0;
}

.ab-social {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    /* Own row inside .ab-who. */
    width: 100%;
    margin: 0;
    padding: 0;
    list-style: none;
}

.ab-social a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    border: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
    color: var(--re-text-color);
    opacity: 0.6;
    font-size: 0.95rem;
    text-decoration: none;
    transition: opacity 0.16s ease, border-color 0.16s ease, background 0.16s ease;
}

.ab-social a:hover {
    opacity: 1;
    border-color: rgba(186, 158, 187, 0.5);
    background: rgba(186, 158, 187, 0.14);
    color: var(--re-text-color);
}

/* --- prose ---------------------------------------------------------- */

.ab-prose {
    font-family: var(--re-font-body);
    font-size: clamp(0.95rem, 1.2vw, 1.05rem);
    line-height: 1.65;
    color: var(--re-text-color);
    opacity: 0.78;
    max-width: 68ch;
    margin: 0;
}

/* --- game shelves ---------------------------------------------------- */

/*
 * Two rails inside one section, where there used to be two sections.
 *
 * .ab-shelf-title is a rank below .page-section-title and has to look it, so
 * it takes the display face at roughly half the size rather than becoming
 * another heading competing with the one above it.
 */
.ab-shelf + .ab-shelf {
    margin-top: clamp(28px, 3.5vw, 42px);
    padding-top: clamp(24px, 3vw, 34px);
    border-top: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
}

.ab-shelf-title {
    font-family: var(--re-font-display) !important;
    font-size: clamp(0.95rem, 1.5vw, 1.15rem);
    line-height: 1.1;
    letter-spacing: 0.02em;
    color: var(--re-text-color);
    opacity: 0.8;
    margin: 0 0 14px;
}

/* --- skills and experience (shared with Resume) --------------------- */

/*
 * .cv-* rather than .ab-*: About and Resume render the same two tables
 * from the same model, so a job or a skill looks identical on both. The
 * prefix says shared, not "belongs to About".
 */

.cv-skills {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: clamp(24px, 3vw, 40px);
    list-style: none;
    margin: 0;
    padding: 0;
}

.cv-skill-cat > h3 {
    font-family: var(--re-font-ui) !important;
    font-size: 0.58rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.45;
    margin: 0 0 14px;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
}

.cv-skill-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.cv-skill {
    margin-bottom: 13px;
}

.cv-skill-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 6px;
}

.cv-skill-name {
    font-family: var(--re-font-body);
    font-size: 0.86rem;
    color: var(--re-text-color);
    opacity: 0.85;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.cv-skill-pct {
    font-family: var(--re-font-ui);
    font-size: 0.5rem;
    color: var(--re-text-color);
    opacity: 0.4;
    font-variant-numeric: tabular-nums;
    flex: 0 0 auto;
}

/*
 * Deliberately the same rail as the player's progress and volume bars in
 * site-chrome.css: 4px tall, 2px radius, brand-400 at 0.28 behind a brand-200
 * fill. A proficiency meter and a seek bar are both "how far along this is",
 * and there is no reason for the site to draw that idea two ways.
 */
.cv-skill-track {
    height: 4px;
    border-radius: 2px;
    background: rgba(var(--re-brand-400-rgb), 0.28);
    overflow: hidden;
}

.cv-skill-fill {
    height: 100%;
    border-radius: 2px;
    background: var(--re-brand-200);
}

/* --- experience ----------------------------------------------------- */

.cv-exp {
    list-style: none;
    margin: 0;
    padding: 0;
    border-top: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
}

.cv-exp > li {
    display: grid;
    /* The date rail is its own column so every role's title starts on the same
       line, however long the dates are. */
    grid-template-columns: minmax(0, 150px) minmax(0, 1fr);
    gap: 6px clamp(18px, 2.5vw, 34px);
    padding: clamp(16px, 2vw, 22px) 0;
    border-bottom: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
}

.cv-exp-when {
    font-family: var(--re-font-ui);
    font-size: 0.54rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.42;
    padding-top: 4px;
}

.cv-exp-role {
    font-family: var(--re-font-body);
    font-size: 1rem;
    font-weight: 600;
    color: var(--re-text-color);
    margin: 0 0 3px;
}

.cv-exp-where {
    font-family: var(--re-font-ui);
    font-size: 0.52rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.5;
    margin: 0 0 10px;
}

.cv-exp-desc {
    font-family: var(--re-font-body);
    font-size: 0.9rem;
    line-height: 1.6;
    color: var(--re-text-color);
    opacity: 0.68;
    margin: 0 0 12px;
}

.cv-exp-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 8px;
    list-style: none;
    margin: 0;
    padding: 0;
}

.cv-exp-tags li {
    font-family: var(--re-font-ui);
    font-size: 0.48rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.55;
    padding: 4px 9px;
    border: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
    border-radius: 999px;
}

/* --- narrow --------------------------------------------------------- */

@media (max-width: 720px) {
    /*
     * One column: at this width the bio has nowhere to sit beside the avatar,
     * and 220px of name next to 100px of paragraph helps nobody.
     */
    .ab-identity {
        grid-template-columns: minmax(0, 1fr);
        gap: 20px;
    }

    .ab-bio {
        padding-top: 0;
    }

    /* Dates above the role rather than beside it - a 150px rail is most of a
       375px screen. */
    .cv-exp > li {
        grid-template-columns: minmax(0, 1fr);
        gap: 4px;
    }

    .cv-exp-when {
        padding-top: 0;
    }
}

/* ------------------------------------------------------------------ *
 * Resume
 * ------------------------------------------------------------------ */

/*
 * Resume already had the shared masthead and section heads. What it still had
 * underneath was the old card language: every entry a `.glass .card` panel
 * with about a dozen inline style attributes each, dates and tags as
 * `.badge.glass-light` in 8px Silkscreen, and every icon coloured
 * var(--bs-link-hover-color) - a Bootstrap link colour, not one of this site's
 * brand tokens, which is why the accents on this page were a different hue
 * from every other page.
 *
 * The entries are hairline-separated rows now, like About and Music. Nothing
 * on this page is a panel any more.
 *
 * Experience and Skills are NOT here: they are .cv-* and shared with About,
 * because both pages render the same two tables from the same model and there
 * is no reason for a job to look like one thing on one page and another
 * thing on the next.
 */

.rs-contact {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px 20px;
    padding: 0 0 clamp(20px, 3vw, 30px);
    margin-bottom: clamp(28px, 4vw, 44px);
    border-bottom: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
    font-family: var(--re-font-ui);
    font-size: 0.56rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.rs-contact a,
.rs-contact > span {
    color: var(--re-text-color);
    opacity: 0.6;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    transition: opacity 0.16s ease;
}

.rs-contact a:hover {
    opacity: 1;
    color: var(--re-text-color);
}

/* --- education ------------------------------------------------------ */

.rs-list {
    list-style: none;
    margin: 0;
    padding: 0;
    border-top: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
}

.rs-list > li {
    display: grid;
    grid-template-columns: minmax(0, 150px) minmax(0, 1fr);
    gap: 6px clamp(18px, 2.5vw, 34px);
    padding: clamp(16px, 2vw, 22px) 0;
    border-bottom: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
}

.rs-when {
    font-family: var(--re-font-ui);
    font-size: 0.54rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.42;
    padding-top: 4px;
}

.rs-title {
    font-family: var(--re-font-body);
    font-size: 1rem;
    font-weight: 600;
    color: var(--re-text-color);
    margin: 0 0 3px;
}

.rs-where {
    font-family: var(--re-font-ui);
    font-size: 0.52rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.5;
    margin: 0 0 10px;
}

.rs-body {
    font-family: var(--re-font-body);
    font-size: 0.9rem;
    line-height: 1.6;
    color: var(--re-text-color);
    opacity: 0.68;
    margin: 0 0 12px;
}

/*
 * Key achievements, and the quote in a recommendation. A rule down the left
 * rather than a tinted box: the old version stacked a panel inside a panel,
 * and a single line is enough to say "this is set apart".
 */
.rs-aside {
    border-left: 2px solid rgba(186, 158, 187, 0.45);
    padding: 2px 0 2px 16px;
    margin: 0 0 14px;
}

.rs-aside-label {
    display: block;
    font-family: var(--re-font-ui);
    font-size: 0.5rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.45;
    margin-bottom: 6px;
}

.rs-aside p {
    font-family: var(--re-font-body);
    font-size: 0.88rem;
    line-height: 1.6;
    color: var(--re-text-color);
    opacity: 0.7;
    margin: 0;
}

/* --- recommendations ------------------------------------------------ */

.rs-recs {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: clamp(24px, 3vw, 38px);
    list-style: none;
    margin: 0;
    padding: 0;
}

.rs-rec-quote {
    font-family: var(--re-font-body);
    font-size: 0.92rem;
    line-height: 1.65;
    font-style: italic;
    color: var(--re-text-color);
    opacity: 0.78;
    border-left: 2px solid rgba(186, 158, 187, 0.45);
    padding: 2px 0 2px 16px;
    margin: 0 0 16px;
}

.rs-rec-who {
    font-family: var(--re-font-body);
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--re-text-color);
    margin: 0 0 3px;
}

.rs-rec-role {
    font-family: var(--re-font-ui);
    font-size: 0.5rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.5;
    margin: 0 0 10px;
}

/* --- narrow --------------------------------------------------------- */

@media (max-width: 720px) {
    /* A 150px date rail is most of a 375px screen; dates go above instead. */
    .rs-list > li {
        grid-template-columns: minmax(0, 1fr);
        gap: 4px;
    }

    .rs-when {
        padding-top: 0;
    }
}

/* --- lab ------------------------------------------------------------ */

/*
 * Lab reuses the Portfolio card wholesale - it is the same shape of thing, a
 * titled project with a description, tags and links. Only what Lab has and
 * Portfolio does not lives here.
 */

.lb-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 8px;
    margin-bottom: 10px;
}

/*
 * Type and status ride above the title. They used to be absolutely positioned
 * over the cover image, which meant a card without one showed neither - and
 * most Lab entries have no cover image yet.
 */
.lb-tag {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-family: var(--re-font-ui);
    font-size: 0.48rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.55;
    padding: 4px 9px;
    border: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
    border-radius: 999px;
}

/*
 * The dot is the only place a status keeps its own colour. The old cards set
 * the whole label to it - four saturated hues (#28a745, #007bff, #dc3545...)
 * against a mauve page, none of them from the palette. A 6px dot says the
 * same thing without pulling the page apart.
 */
.lb-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    flex: 0 0 auto;
    display: inline-block;
}

.lb-updated {
    font-family: var(--re-font-ui);
    font-size: 0.48rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.35;
    margin: 0 0 14px;
}

/* --- search and sort, beside the filter pills ----------------------- */

/*
 * The Projects page needs two controls the other listings do not: a search box
 * and a sort order. They sit on one line under the pills rather than in a
 * panel of their own, and they are built to read as part of the same row -
 * same height, same hairline border, same uppercase UI face.
 */

.pg-tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 10px;
    margin: 0 0 clamp(24px, 3vw, 36px);
}

.pg-search {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex: 1 1 240px;
    /* min-width: 0 so the field can shrink below its intrinsic size instead of
       pushing the sort and buttons onto their own line at every width. */
    min-width: 0;
    padding: 6px 14px;
    border: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
    border-radius: 999px;
    color: var(--re-text-color);
    opacity: 0.72;
    transition: border-color 0.16s ease, opacity 0.16s ease;
}

.pg-search:focus-within {
    opacity: 1;
    border-color: rgba(186, 158, 187, 0.55);
}

.pg-search input {
    flex: 1 1 auto;
    min-width: 0;
    background: none;
    border: none;
    outline: none;
    color: var(--re-text-color);
    font-family: var(--re-font-body);
    font-size: 0.85rem;
}

.pg-search input::placeholder {
    color: var(--re-text-color);
    opacity: 0.4;
}

/*
 * select.pg-select, not .pg-select. projects-page.css carries
 * select[name="sort"] - a type plus an attribute, which is (0,1,1) against
 * a bare class's (0,1,0) and wins however late this file loads. Adding the
 * element name matches its weight, and loading later then settles it.
 */
select.pg-select {
    padding: 7px 14px;
    border: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
    border-radius: 999px;
    /* The native control paints its own ground; without this it is a white
       slab in the middle of the row on every browser but Firefox. */
    background: transparent;
    color: var(--re-text-color);
    font-family: var(--re-font-ui);
    font-size: 0.56rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    opacity: 0.72;
    cursor: pointer;
}

select.pg-select:hover,
select.pg-select:focus {
    opacity: 1;
    border-color: rgba(186, 158, 187, 0.55);
    outline: none;
}

/* The dropdown itself is drawn by the OS, which does not inherit the page. */
select.pg-select option {
    background: #1a141c;
    color: #d8c8d6;
}

/* A <button> needs the reset a link does not. */
button.pg-chip {
    background: transparent;
    font: inherit;
    font-family: var(--re-font-ui);
    font-size: 0.56rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
}

/* ------------------------------------------------------------------ *
 * Bulk music upload (admin)
 * ------------------------------------------------------------------ */

.bulk-drop {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: clamp(34px, 6vw, 64px) 20px;
    border: 1px dashed var(--page-rule, rgba(130, 107, 125, 0.28));
    border-radius: var(--re-radius-lg, 14px);
    text-align: center;
    transition: border-color 0.16s ease, background 0.16s ease;
}

/* Only while something is actually over it - a permanent highlight reads as
   "this is busy" when nothing is happening. */
.bulk-drop.is-over {
    border-color: rgba(186, 158, 187, 0.7);
    background: rgba(186, 158, 187, 0.09);
}

.bulk-drop > i {
    font-size: 2rem;
    opacity: 0.35;
}

.bulk-drop-lead {
    font-family: var(--re-font-body);
    font-size: 0.98rem;
    color: var(--re-text-color);
    opacity: 0.8;
    margin: 0;
}

.bulk-drop-note {
    font-family: var(--re-font-ui);
    font-size: 0.5rem;
    letter-spacing: 0.12em;
    color: var(--re-text-color);
    opacity: 0.35;
    margin: 0;
}

.bulk-hint,
.bulk-options {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 20px;
    margin: 16px 0 0;
    font-family: var(--re-font-body);
    font-size: 0.84rem;
    color: var(--re-text-color);
    opacity: 0.62;
}

.bulk-hint code {
    font-size: 0.92em;
    padding: 1px 5px;
    border-radius: 4px;
    background: var(--re-surface-hover);
}

.bulk-options label {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
}

/* --- progress ------------------------------------------------------- */

.bulk-run {
    margin: 22px 0 6px;
}

/* Same 4px rail as the player and the skill meters. */
.bulk-bar {
    height: 4px;
    border-radius: 2px;
    background: rgba(var(--re-brand-400-rgb), 0.28);
    overflow: hidden;
}

.bulk-bar-fill {
    height: 100%;
    width: 0;
    border-radius: 2px;
    background: var(--re-brand-200);
    transition: width 0.2s ease;
}

.bulk-status {
    font-family: var(--re-font-ui);
    font-size: 0.56rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--re-text-color);
    opacity: 0.5;
    margin: 10px 0 0;
}

/* --- results -------------------------------------------------------- */

.bulk-results {
    list-style: none;
    margin: 18px 0 40px;
    padding: 0;
}

.bulk-result {
    display: grid;
    /* minmax(0,...) on both, so a long filename ellipsises instead of pushing
       the detail column off the row. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
    gap: 8px 18px;
    padding: 9px 0 9px 12px;
    border-bottom: 1px solid var(--page-rule, rgba(130, 107, 125, 0.18));
    border-left: 2px solid transparent;
    font-family: var(--re-font-body);
    font-size: 0.85rem;
}

.bulk-result-name {
    color: var(--re-text-color);
    opacity: 0.8;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.bulk-result-detail {
    color: var(--re-text-color);
    opacity: 0.55;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The left rule is the whole status signal: four states, no icons to decode. */
.bulk-result.is-ok      { border-left-color: rgba(95, 191, 114, 0.75); }
.bulk-result.is-library { border-left-color: rgba(186, 158, 187, 0.75); }
.bulk-result.is-skip    { border-left-color: rgba(154, 146, 164, 0.6); }
.bulk-result.is-error   { border-left-color: rgba(217, 112, 112, 0.85); }
.bulk-result.is-error .bulk-result-detail { opacity: 0.85; }

@media (max-width: 720px) {
    .bulk-result {
        grid-template-columns: minmax(0, 1fr);
        gap: 2px;
    }
}


/* ------------------------------------------------------------------ *
 * Music page: admin quick actions
 * ------------------------------------------------------------------ */

/*
 * Rendered only for an admin, so the extra column only exists then - hence
 * the .has-admin class rather than :has(.mu-admin). A visitor's grid is
 * untouched, and this does not depend on :has() support to lay out correctly.
 */
.mu-track.has-admin {
    grid-template-columns: 30px 52px minmax(0, 1fr) auto auto auto auto;
}

.mu-admin {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

.mu-admin-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 30px: smaller than the 44px a thumb needs, because these sit inside a
       dense list on a screen with a mouse, and larger targets here would push
       the track title's column in. Still comfortably clickable. */
    width: 30px;
    height: 30px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--re-radius-sm);
    background: none;
    color: var(--re-text-color);
    opacity: 0.35;
    cursor: pointer;
    text-decoration: none;
    font-size: 13px;
    line-height: 1;
    transition: opacity var(--re-transition), background var(--re-transition),
                border-color var(--re-transition), color var(--re-transition);
}

/*
 * Faint until the row is hovered. A list of 368 tracks with three bright
 * icons on every line is a control panel, not a list of music - they should
 * be there when you reach for them and quiet when you are not.
 */
.mu-track:hover .mu-admin-btn,
.mu-admin-btn:focus-visible {
    opacity: 0.75;
}

.mu-admin-btn:hover {
    opacity: 1;
    background: var(--re-surface-hover);
    border-color: var(--re-border);
}

/* Featured: on is the loud state. */
.mu-admin-btn.is-on {
    opacity: 0.95;
    color: var(--re-brand-200);
}

/* Draft: the eye is struck through and stays visible, because it is telling
   you something is wrong rather than offering an action. */
.mu-admin-btn.is-off {
    opacity: 0.9;
    color: #e0a13a;
}

.mu-admin-btn.is-error {
    color: #e06a5a;
    border-color: #e06a5a;
    opacity: 1;
}

.mu-admin-btn:disabled { cursor: progress; }

/* ------------------------------------------------------------------ *
 * Admin: default playlist order
 * ------------------------------------------------------------------ */

.mq-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 20px;
    margin-bottom: 20px;
    flex-wrap: wrap;
}

.mq-title {
    color: var(--re-brand-200);
    font-size: 24px;
    margin: 0 0 6px;
    font-family: 'Albert Sans', sans-serif;
}

.mq-sub {
    margin: 0;
    max-width: 46ch;
    color: var(--re-text-color);
    opacity: 0.7;
    font-size: 0.8rem;
}

.mq-btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 10px 18px;
    border: 1px solid rgba(130, 107, 125, 0.45);
    border-radius: var(--re-radius-md);
    background: rgba(130, 107, 125, 0.28);
    color: var(--re-text-color);
    text-decoration: none;
    /* No font-size: silkscreen-pixel-grid.css settles every Silkscreen run at
       8px, so a number here would only be a lie about what renders. Tracking
       is a whole pixel - 0.5px puts the next glyph off the grid. */
    font-family: 'Silkscreen', serif;
    text-transform: uppercase;
    letter-spacing: 1px;
    cursor: pointer;
}

.mq-btn:hover { background: rgba(130, 107, 125, 0.45); }

.mq-btn-go {
    background: #5d4b60;
    border-color: #5d4b60;
    color: #fff;
}

/* Nothing has moved yet, so there is nothing to save. */
.mq-btn-go:disabled {
    opacity: 0.35;
    cursor: default;
}

.mq-bar {
    position: sticky;
    top: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 12px 0;
    margin-bottom: 8px;
    background: var(--re-bg, #14121a);
    border-bottom: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
}

.mq-count {
    font-family: var(--re-font-ui);
    text-transform: uppercase;
    letter-spacing: 1px;
    opacity: 0.6;
}

.mq-msg {
    padding: 12px 16px;
    border-radius: var(--re-radius-md);
    border: 1px solid transparent;
    font-size: 0.85rem;
}

.mq-msg-success {
    background: rgba(95, 191, 114, 0.12);
    border-color: rgba(95, 191, 114, 0.5);
}

.mq-msg-error {
    background: rgba(224, 106, 90, 0.12);
    border-color: rgba(224, 106, 90, 0.5);
}

.mq-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.mq-row {
    display: grid;
    /* grip, number, art, text, arrows */
    grid-template-columns: 24px 34px 40px minmax(0, 1fr) auto;
    align-items: center;
    gap: 14px;
    padding: 9px 10px;
    border-bottom: 1px solid var(--page-rule, rgba(130, 107, 125, 0.22));
    background: var(--re-bg, transparent);
}

.mq-row:hover { background: var(--re-surface-raised); }

/*
 * The row being dragged stays in the list and is dimmed, rather than being
 * lifted out of it. The gap it would leave is the whole point: the list
 * reflows live as the pointer moves, so what you see during the drag is what
 * you get when you let go.
 */
.mq-row.is-dragging {
    opacity: 0.4;
    background: rgba(186, 158, 187, 0.14);
}

.mq-grip {
    cursor: grab;
    text-align: center;
    opacity: 0.4;
    font-size: 15px;
}

.mq-row:hover .mq-grip { opacity: 0.8; }
.mq-grip:active { cursor: grabbing; }

.mq-pos {
    font-family: var(--re-font-ui);
    text-align: right;
    opacity: 0.5;
    font-variant-numeric: tabular-nums;
}

.mq-art {
    width: 40px;
    height: 40px;
    object-fit: cover;
    border-radius: var(--re-radius-sm);
    background: var(--re-surface-raised);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    opacity: 0.85;
}

.mq-text  { min-width: 0; display: grid; gap: 2px; }
.mq-name  { font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mq-by    { font-size: 0.72rem; opacity: 0.55; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.mq-moves { display: inline-flex; gap: 2px; }

.mq-move {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* 34px: these are the way this is done on a phone, where dragging inside
       a scrolling page is a fight, so they get a real target. */
    width: 34px;
    height: 34px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--re-radius-sm);
    background: none;
    color: var(--re-text-color);
    opacity: 0.4;
    cursor: pointer;
    font-size: 13px;
}

.mq-row:hover .mq-move,
.mq-move:focus-visible { opacity: 0.85; }

.mq-move:hover {
    opacity: 1;
    background: var(--re-surface-hover);
    border-color: var(--re-border);
}

.mq-move:disabled {
    opacity: 0.12;
    cursor: default;
    background: none;
    border-color: transparent;
}

@media (hover: none) {
    .mq-grip,
    .mq-move { opacity: 0.7; }
}

@media (max-width: 720px) {
    .mq-row {
        /* The artwork goes; the number, the title and the arrows are what
           this page is for. */
        grid-template-columns: 20px 28px minmax(0, 1fr) auto;
        gap: 10px;
    }
    .mq-art { display: none; }
}

/* ------------------------------------------------------------------ *
 * Music page: add to / remove from the playlist
 * ------------------------------------------------------------------ */

/*
 * The only control on a row that changes the queue.
 *
 * Pressing the row plays the track and leaves the playlist alone; this is how
 * a track gets into it, and how it comes back out. Both in one button,
 * because the useful thing to show is the state - a plus when the track is
 * not in the list, a tick when it is - rather than two icons the reader has
 * to choose between.
 *
 * 30px to match .mu-admin-btn beside it. Dense list, pointer device; see the
 * note there.
 */
.mu-queue-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--re-radius-sm);
    background: none;
    color: var(--re-text-color);
    opacity: 0.3;
    cursor: pointer;
    font-size: 13px;
    line-height: 1;
    transition: opacity var(--re-transition), background var(--re-transition),
                border-color var(--re-transition), color var(--re-transition);
}

.mu-track:hover .mu-queue-btn,
.mu-queue-btn:focus-visible {
    opacity: 0.75;
}

.mu-queue-btn:hover {
    opacity: 1;
    background: var(--re-surface-hover);
    border-color: var(--re-border);
}

/*
 * In the playlist. Loud, and loud whether or not the row is hovered - this is
 * the one piece of information the button exists to carry, and hiding it
 * until the pointer arrives would mean scanning the list by hovering it.
 */
.mu-queue-btn.is-queued {
    opacity: 1;
    color: var(--re-brand-200);
    border-color: rgba(186, 158, 187, 0.45);
    background: rgba(186, 158, 187, 0.12);
}

.mu-queue-btn.is-queued:hover {
    color: #ffb4a8;
    border-color: rgba(224, 106, 90, 0.55);
    background: rgba(224, 106, 90, 0.16);
}

/* A short pulse on the press, so the change registers on a long list where
   the button may be the only thing that moved. */
.mu-queue-btn.just-changed {
    animation: mu-queue-pop 0.32s ease;
}

@keyframes mu-queue-pop {
    0%   { transform: scale(1); }
    45%  { transform: scale(1.28); }
    100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
    .mu-queue-btn.just-changed { animation: none; }
}

/*
 * Touch has no hover, so the plus is simply visible - faint, but there. On a
 * phone it is otherwise invisible until pressed, which is no use at all.
 */
@media (hover: none) {
    .mu-queue-btn { opacity: 0.5; }
}

/*
 * A drafted row, seen only by an admin. Dimmed and striped so it is obvious
 * at a glance which tracks the public cannot see, without removing it from
 * the list you are working in.
 */
.mu-track.is-draft {
    opacity: 0.62;
    background-image: repeating-linear-gradient(
        135deg,
        transparent 0 8px,
        rgba(224, 161, 58, 0.05) 8px 16px);
}

.mu-track.is-draft .mu-title::after {
    content: "DRAFT";
    display: inline-block;
    margin-left: 8px;
    padding: 1px 5px;
    border: 1px solid rgba(224, 161, 58, 0.5);
    border-radius: 3px;
    color: #e0a13a;
    font-family: var(--re-font-ui);
    letter-spacing: 1px;
    vertical-align: middle;
}
