/* components.css — Past Lives shared component library
 *
 * Loaded by both hub/base.html and admin/base.html.
 * All classes use the pl- prefix.
 * Theme-aware via [data-theme="light"] selectors and CSS variables from hub.css.
 */

/* ========================================
   Modal
   ======================================== */
.pl-modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.6);
    padding: 1rem;
}

.pl-modal {
    background: var(--hub-card-bg, #0f2d44);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 12px;
    width: 100%;
    max-height: 85vh;
    overflow-y: auto;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
}

.pl-modal--sm { max-width: 400px; }
.pl-modal--md { max-width: 560px; }
.pl-modal--lg { max-width: 720px; }

.pl-modal__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1.25rem 1.5rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}

.pl-modal__title {
    font-family: 'Lato', system-ui, sans-serif;
    font-size: 1.125rem;
    font-weight: 700;
    color: var(--hub-text, #F4EFDD);
    margin: 0;
}

.pl-modal__close {
    background: none;
    border: none;
    color: var(--hub-text-muted, #96ACBB);
    font-size: 1.5rem;
    cursor: pointer;
    padding: 0;
    line-height: 1;
}

.pl-modal__close:hover {
    color: var(--hub-text, #F4EFDD);
}

.pl-modal__body {
    padding: 1.5rem;
}

.pl-modal__actions {
    display: flex;
    gap: 0.75rem;
    margin-top: 1.5rem;
}

/* Text input inside a modal (the confirm modal's note and typed-confirm fields). Mirrors
   .hub-form-group's control styling on the theme's input tokens so it never renders as the
   browser-default white box on the dark theme (FRONTEND.md Rule 13). */
.pl-input {
    width: 100%;
    padding: 0.625rem 0.875rem;
    font-family: var(--font-body);
    font-size: 0.9375rem;
    font-weight: 300;
    color: var(--hub-text);
    background-color: var(--hub-input-bg);
    border: 1px solid var(--hub-input-border);
    border-radius: 6px;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.pl-input:focus {
    outline: none;
    border-color: var(--hub-blue);
    box-shadow: 0 0 0 3px rgba(61, 139, 212, 0.25);
}
.pl-input::placeholder {
    color: var(--hub-text-muted);
}

/* Opt-in: action buttons share the row's width equally (inline-form modals). */
.pl-modal__actions--fill .pl-btn {
    flex: 1;
}

/* First-login welcome modal (nudges profile setup) */
.pl-welcome-modal__lead {
    margin: 0;
    color: var(--hub-text-muted, #96ACBB);
    line-height: 1.6;
    font-size: 0.95rem;
}

.pl-welcome-modal__actions {
    justify-content: flex-end;
    flex-wrap: wrap;
}

.pl-welcome-modal__actions form {
    margin: 0;
}

/* Modal transitions */
.modal-enter {
    transition: opacity 0.2s ease;
}

.modal-leave {
    transition: opacity 0.15s ease;
}

/* Toast styles live inline in templates/components/toast.html (self-contained, `plt-` prefix). */

/* ========================================
   Toggle
   ======================================== */
.pl-toggle-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 1rem 0;
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}

.pl-toggle-row:last-child {
    border-bottom: none;
}

.pl-toggle-label {
    font-size: 0.9375rem;
    font-weight: 400;
    color: var(--hub-text, #F4EFDD);
}

.pl-toggle-desc {
    font-size: 0.8125rem;
    color: var(--hub-text-muted, #96ACBB);
    margin-top: 0.125rem;
}

.pl-toggle {
    position: relative;
    display: inline-block;
    width: 44px;
    height: 24px;
    flex-shrink: 0;
}

.pl-toggle input {
    opacity: 0;
    width: 0;
    height: 0;
    position: absolute;
}

.pl-toggle__slider {
    position: absolute;
    cursor: pointer;
    inset: 0;
    background-color: rgba(255, 255, 255, 0.15);
    border-radius: 24px;
    transition: background-color 0.2s ease;
}

.pl-toggle__slider::before {
    content: "";
    position: absolute;
    height: 18px;
    width: 18px;
    left: 3px;
    bottom: 3px;
    background-color: #fff;
    border-radius: 50%;
    transition: transform 0.2s ease;
}

.pl-toggle input:checked + .pl-toggle__slider {
    background-color: var(--color-tuscan-yellow, #EEB44B);
}

.pl-toggle input:checked + .pl-toggle__slider::before {
    transform: translateX(20px);
}

.pl-toggle input:focus-visible + .pl-toggle__slider {
    box-shadow: 0 0 0 3px rgba(238, 180, 75, 0.3);
}

/* Compact toggle for dense grids (the notification matrix). */
.pl-toggle--sm {
    width: 36px;
    height: 20px;
}

.pl-toggle--sm .pl-toggle__slider::before {
    height: 14px;
    width: 14px;
    left: 3px;
    bottom: 3px;
}

.pl-toggle--sm input:checked + .pl-toggle__slider::before {
    transform: translateX(16px);
}

/* Locked-on toggle: a forced / always-on channel the user can't change. Reads as on
   but visibly inert (no pointer, dimmed) so it's clearly not interactive. */
.pl-toggle--locked {
    cursor: default;
}

.pl-toggle--locked .pl-toggle__slider {
    cursor: default;
    opacity: 0.55;
    background-color: var(--color-tuscan-yellow, #EEB44B);
}

.pl-toggle--locked .pl-toggle__slider::before {
    transform: translateX(16px);
}

/* Unavailable toggle: an opt-in channel the member can't use yet (the Discord DM
   channel before they link Discord). Reads as inert + dimmed, distinct from the
   locked-ON forced style — there's no lock glyph and it doesn't force the knob on. */
.pl-toggle--disabled {
    cursor: default;
}

.pl-toggle--disabled .pl-toggle__slider {
    cursor: default;
    opacity: 0.4;
}

/* A small lock glyph sits in the knob to read as "always on, not interactive". */
.pl-toggle__lock {
    position: absolute;
    top: 50%;
    left: 3px;
    transform: translate(1px, -50%);
    color: var(--color-navy, #092E4C);
    pointer-events: none;
    z-index: 1;
}

.pl-toggle--sm .pl-toggle__lock {
    transform: translate(18px, -50%);
}

/* ========================================
   Notification preference matrix (event × channel)
   ======================================== */
/* Horizontal-scroll wrapper: on narrow screens the matrix scrolls as a unit
   rather than crushing its columns (matches the .admin-table-wrap pattern). */
.pl-notif-matrix__scroll {
    border: 1px solid var(--hub-border, rgba(255, 255, 255, 0.1));
    border-radius: 8px;
    overflow-x: auto;
}

.pl-notif-matrix {
    /* Column count is set per-instance via --pl-notif-cols (the rendered channel
       count) so the grid always matches the template and never hardcodes a number. */
    --pl-notif-cols: 5;
    display: grid;
    /* The event column flexes; the fixed channel columns set an intrinsic floor,
       so when they don't fit, the __scroll wrapper takes over rather than crushing them.
       Width fits the two-word labels — "Push (Browser)" wraps to "PUSH / (BROWSER)". */
    grid-template-columns: minmax(200px, 1fr) repeat(var(--pl-notif-cols), 84px);
    align-items: stretch;
}

.pl-notif-matrix__head,
.pl-notif-matrix__row {
    display: contents;
}

.pl-notif-matrix__event-head,
.pl-notif-matrix__chan-head {
    background: var(--hub-surface, rgba(255, 255, 255, 0.04));
    color: var(--hub-text-muted, #8b97a8);
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    padding: 0.5rem 0.25rem;
    /* Center and allow a long label to wrap rather than clip inside the fixed
       64px column (this is what cut "Scheduled" down to "SCHEDUL"). */
    text-align: center;
    white-space: normal;
    overflow-wrap: anywhere;
    line-height: 1.15;
    border-bottom: 1px solid var(--hub-border, rgba(255, 255, 255, 0.1));
}

.pl-notif-matrix__event-head {
    text-align: left;
}

.pl-notif-matrix__chan-head {
    text-align: center;
}

.pl-notif-matrix__event,
.pl-notif-matrix__cell {
    padding: 0.6rem 0.5rem;
    border-bottom: 1px solid var(--hub-border, rgba(255, 255, 255, 0.08));
}

/* Drop the last row's bottom border (the container's border closes it). */
.pl-notif-matrix__row:last-child .pl-notif-matrix__event,
.pl-notif-matrix__row:last-child .pl-notif-matrix__cell {
    border-bottom: none;
}

.pl-notif-matrix__event-label {
    font-weight: 500;
    font-size: 0.875rem;
    color: var(--hub-text, #F4EFDD);
}

.pl-notif-matrix__event-desc {
    font-size: 0.72rem;
    margin-top: 0.1rem;
}

.pl-notif-matrix__event-badge {
    display: inline-block;
    margin-top: 0.3rem;
    padding: 0.1rem 0.45rem;
    border-radius: 999px;
    font-size: 0.68rem;
    font-weight: 600;
    color: var(--hub-accent, #EEB44B);
    background: rgba(238, 180, 75, 0.14);
}

.pl-notif-matrix__cell {
    display: flex;
    align-items: center;
    justify-content: center;
}

.pl-notif-matrix__na {
    color: var(--hub-text-muted, #8b97a8);
    opacity: 0.4;
    font-size: 0.85rem;
}

@media (max-width: 640px) {
    .pl-notif-matrix {
        /* Tighter on phones; the __scroll wrapper carries the horizontal overflow. */
        grid-template-columns: minmax(150px, 1fr) repeat(var(--pl-notif-cols), 68px);
    }
}

/* Notifications tab chrome — replaces one-off inline styles. */
.pl-notif-title {
    font-size: 1rem;
    font-weight: 600;
    margin: 0 0 0.25rem;
}

.pl-notif-intro {
    margin-bottom: 1rem;
    font-size: 0.85rem;
}

/* "Push On This Device" card, above the notifications matrix. Only the native app ever
   reveals it (static/js/native-push.js clears [hidden]), so the default is hidden and the
   rule is explicit rather than leaning on the UA stylesheet, which .hub-card could outrank. */
.pl-push-device[hidden] {
    display: none;
}

/* The `hidden` attribute is ONLY a UA-stylesheet `display: none`, so any author rule that sets
   display outranks it and the element stays on screen no matter what the script sets. That is
   real here: `.pl-btn` is `display: inline-flex`, so hiding a button by attribute alone did
   nothing and both Turn On and Turn Off showed at once. One rule for every descendant the
   script toggles, so elements added to this card later are covered too. */
.pl-push-device [hidden] {
    display: none;
}

.pl-push-device {
    margin-bottom: 1rem;
}

.pl-push-device__title {
    font-size: 1rem;
    font-weight: 600;
    margin: 0 0 0.5rem;
}

.pl-push-device__status {
    margin: 0;
    font-size: 0.9rem;
    color: var(--hub-text, #F4EFDD);
}

/* The blocked state: the only one a member cannot fix from inside the app, so it gets
   room for the plain instructions rather than being crammed onto the status line. */
.pl-push-device__help {
    margin: 0.5rem 0 0;
    font-size: 0.85rem;
    color: var(--hub-text-muted, #8b97a8);
}

/* display lives here, not inline, so toggling [hidden] cannot strand a display:flex on it.
   The card-wide `.pl-push-device [hidden]` rule above is what actually hides it. */
.pl-push-device__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 1rem;
}

.pl-push-device__others {
    margin: 1rem 0 0;
    font-size: 0.85rem;
}

/* Discord account-linking control on the Notifications tab: a themed banner with the
   linked status on the left and the Connect/Disconnect action on the right. Uses the
   hub surface/border/text tokens so it adapts to both Obsidian (dark) and Slate
   (light) themes, like the matrix below it. */
.pl-discord-link {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 0.75rem 1rem;
    margin-bottom: 1.25rem;
    border: 1px solid var(--hub-border, rgba(255, 255, 255, 0.1));
    border-radius: 8px;
    background: var(--hub-surface, rgba(255, 255, 255, 0.04));
}

.pl-discord-link__status {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.85rem;
    color: var(--hub-text, #F4EFDD);
}

.pl-discord-link__icon {
    flex: none;
    color: #5865F2; /* Discord blurple */
}

.pl-discord-link__form {
    margin: 0;
}

.pl-notif-category {
    margin: 0;
}

/* Category header row: title on the left, the per-section All on/off control on
   the right. */
.pl-notif-category-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin: 1.25rem 0 0.5rem;
}

.pl-notif-section + .pl-notif-section {
    margin-top: 0.5rem;
}

/* Sub-heading note under the Staff & leadership section explaining who sees it. */
.pl-notif-section-note {
    margin: 0 0 0.75rem;
    font-size: 0.85rem;
}

/* Anchor offset so a jumped-to section clears the sticky topbar. */
.pl-notif-section {
    scroll-margin-top: calc(var(--topbar-height) + 1rem);
}

/* Jump-to-section chips above the notification matrix. */
.pl-notif-jump {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0 0 1.25rem;
}
.pl-notif-jump__chip {
    padding: 0.25rem 0.75rem;
    border-radius: 999px;
    font-size: 0.8rem;
    background: var(--hub-surface);
    color: var(--hub-text-muted);
    border: 1px solid transparent;
    text-decoration: none;
}
.pl-notif-jump__chip:hover,
.pl-notif-jump__chip:focus-visible {
    color: var(--hub-text);
    border-color: var(--color-tuscan-yellow);
    text-decoration: none;
}

/* Danger-zone card treatment: red-tinted border + background in both themes, warning
   icon, right-aligned footer button. Reusable, so it lives with the components. */
.pl-danger-zone {
    border: 1px solid rgba(220, 38, 38, 0.45);
    background: rgba(220, 38, 38, 0.07);
}
.pl-danger-zone__head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.75rem;
}
.pl-danger-zone__icon {
    color: #f0a0a0;
    flex-shrink: 0;
}
.pl-danger-zone__title {
    font-size: 1rem;
    font-weight: 700;
    margin: 0;
    color: #f0a0a0;
}
.pl-danger-zone__text {
    margin: 0 0 0.75rem;
    font-size: 0.9rem;
}
.pl-danger-zone__footer {
    display: flex;
    justify-content: flex-end;
    margin-top: 1.25rem;
    padding-top: 1rem;
    border-top: 1px solid rgba(220, 38, 38, 0.25);
}
[data-theme="light"] .pl-danger-zone {
    border-color: rgba(220, 38, 38, 0.35);
    background: rgba(220, 38, 38, 0.05);
}
[data-theme="light"] .pl-danger-zone__icon,
[data-theme="light"] .pl-danger-zone__title {
    color: #b03030;
}

/* Bulk on/off controls (the top "Everything" row and each section's). */
.pl-notif-bulk {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
}

.pl-notif-bulk--all {
    margin-bottom: 0.5rem;
    padding-bottom: 0.75rem;
    border-bottom: 1px solid var(--hub-border, rgba(255, 255, 255, 0.1));
}

.pl-notif-bulk__label {
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--hub-text-muted, #8b97a8);
    margin-right: 0.25rem;
}

.pl-notif-bulk__btn {
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--color-tuscan-yellow, #eeb44b);
}

.pl-notif-bulk__btn:hover {
    text-decoration: underline;
}

.pl-notif-bulk__sep {
    color: var(--hub-text-muted, #8b97a8);
    opacity: 0.5;
}

.pl-notif-actions {
    margin-top: 1.5rem;
}

/* ========================================
   Form Field
   ======================================== */
.pl-form-group {
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
    margin-bottom: 1rem;
}

.pl-form-group:last-child {
    margin-bottom: 0;
}

.pl-form-label {
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--hub-text-muted, #96ACBB);
}

/* The bare input confirm_modal.html renders for its note and typed-confirmation fields.
   That markup has always claimed this class and nothing ever defined it, so both inputs
   shipped as browser-default white boxes with near-invisible text on the Obsidian theme,
   for every caller. Defined here beside .pl-form-label and .pl-field-hint (the same
   markup's other two classes) and mirroring .hub-form-group's input rule, so it inherits
   both themes from the input tokens rather than restyling the labels of three shipped
   screens the way a .hub-form-group wrapper would. */
.pl-input {
    width: 100%;
    padding: 0.625rem 0.875rem;
    font-family: var(--font-body);
    font-size: 0.9375rem;
    color: var(--hub-text);
    background-color: var(--hub-input-bg);
    border: 1px solid var(--hub-input-border);
    border-radius: 6px;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.pl-input:focus {
    outline: none;
    border-color: var(--color-tuscan-yellow);
    box-shadow: 0 0 0 3px rgba(238, 180, 75, 0.15);
}

.pl-form-group input,
.pl-form-group select,
.pl-form-group textarea {
    background: var(--hub-input-bg, rgba(255, 255, 255, 0.06));
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 6px;
    color: var(--hub-text, #F4EFDD);
    padding: 0.5rem 0.75rem;
    font-size: 0.875rem;
    width: 100%;
    transition: border-color 0.15s ease;
}

.pl-form-group input:focus,
.pl-form-group select:focus,
.pl-form-group textarea:focus {
    border-color: var(--color-tuscan-yellow, #EEB44B);
    outline: none;
    box-shadow: 0 0 0 3px rgba(238, 180, 75, 0.15);
}

/* A control with a reason under it: the server's field errors (Django stamps aria-invalid)
   and the composer's per step check (static/js/composer_validation.js) both land here, in
   the same red as .pl-field-error. Focus keeps its gold ring; the border says what is wrong. */
.pl-form-group input[aria-invalid="true"],
.pl-form-group select[aria-invalid="true"],
.pl-form-group textarea[aria-invalid="true"] {
    border-color: #f87171;
}

/* Custom dropdown chevron — replaces the native arrow that crowds the right edge. */
.pl-form-group select {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    padding-right: 2.25rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2396ACBB' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.75rem center;
    background-size: 1rem;
}

/* Native <option> popups don't inherit the select's colors — theme them so the
   dropdown list isn't white-on-white on the dark theme (FRONTEND rule 13). */
.pl-form-group select option {
    background: var(--hub-elevated);
    color: var(--hub-text);
}

/* Date/time pickers: let the native picker indicator (the clock/calendar glyph)
   and the dropdown follow the dark theme — otherwise the glyph renders black and
   is invisible on the dark input background. */
.pl-form-group input[type="time"],
.pl-form-group input[type="date"],
.pl-form-group input[type="datetime-local"] {
    color-scheme: dark;
}

/* File input: theme the native ::file-selector-button — otherwise it renders as an
   OS-default light button on the dark theme. No CSS file styled it before this. */
.pl-form-group input[type="file"]::file-selector-button {
    background: var(--hub-input-bg, rgba(255, 255, 255, 0.06));
    color: var(--hub-text, #F4EFDD);
    border: 1px solid var(--hub-input-border, rgba(255, 255, 255, 0.12));
    border-radius: 6px;
    padding: 0.35rem 0.75rem;
    margin-right: 0.75rem;
    font-size: 0.8125rem;
    cursor: pointer;
}

/* Two-column field layout for long edit forms. Column gap only — each
   .pl-form-group keeps its own margin-bottom for vertical rhythm. Collapses
   to a single column on narrow screens. */
.pl-form-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 1.25rem;
}

@media (max-width: 640px) {
    .pl-form-grid {
        grid-template-columns: 1fr;
    }
}

.pl-field-errors {
    list-style: none;
    margin: 0;
    padding: 0;
}

.pl-field-error {
    font-size: 0.8125rem;
    color: #f87171;
}

.pl-field-hint {
    font-size: 0.8125rem;
    color: var(--hub-text-muted, #96ACBB);
    margin: 0;
}

/* ========================================
   Image Upload Zone
   ======================================== */
.cls-image-upload-zone {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    padding: 1.25rem;
    border: 2px dashed rgba(238, 180, 75, 0.3);
    border-radius: 8px;
    cursor: pointer;
    transition: border-color 0.2s, background 0.2s;
    background: rgba(238, 180, 75, 0.02);
}

/* The zone IS the control: its own label and hint say "choose a file or drag one here",
   so a native file input inside it renders a second, browser-chrome "Choose File / No file
   chosen" on top of that message. Every call site hid it by hand except the wiki's two,
   which is exactly the kind of thing a call site should not have to remember.

   NOT display:none. _wiki_photo_form.html's input is `required`, and a display:none control
   cannot be focused, so Chrome refuses to report it and aborts the submit with nothing but
   "An invalid form control with name='photo' is not focusable." in the console: htmx never
   fires and Add Photo becomes a silently dead button. Zero opacity at 1px keeps the control
   rendered, focusable and reportable while showing nothing, and pointer-events:none stops it
   swallowing clicks meant for the zone's own Remove button. Call sites that already set an
   inline display:none keep it — an inline style wins over this rule either way. */
.cls-image-upload-zone { position: relative; }
.cls-image-upload-zone input[type="file"] {
    position: absolute;
    bottom: 0;
    left: 50%;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.cls-image-upload-zone:hover,
.cls-image-upload-zone.drag-hover {
    border-color: rgba(238, 180, 75, 0.6);
    background: rgba(238, 180, 75, 0.04);
}

.cls-image-upload-zone.uploading {
    opacity: 0.6;
    pointer-events: none;
}

.cls-image-upload-label {
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--color-tuscan-yellow, #EEB44B);
}

.cls-image-upload-hint {
    font-size: 0.75rem;
    color: var(--hub-text-muted, #96ACBB);
}

.cls-image-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 0.75rem;
    margin: 0.5rem 0 0.75rem;
}

.cls-image-cell {
    position: relative;
    background: rgba(9, 46, 76, 0.18);
    border: 1px solid rgba(26, 69, 104, 0.4);
    border-radius: 8px;
    padding: 0.5rem;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    cursor: grab;
    transition: opacity 0.2s, transform 0.15s;
}

.cls-image-cell.dragging { opacity: 0.4; transform: scale(0.95); }
.cls-image-cell.drag-over { border-color: var(--color-tuscan-yellow, #EEB44B); box-shadow: 0 0 0 2px rgba(238, 180, 75, 0.3); }

.cls-image-thumb {
    width: 100%;
    height: 100px;
    border-radius: 6px;
    overflow: hidden;
    background: rgba(255, 255, 255, 0.04);
}

.cls-image-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

.cls-image-fields {
    display: flex;
    gap: 0.35rem;
    align-items: center;
}

.cls-image-alt {
    flex: 1;
    padding: 0.3rem 0.5rem;
    font-size: 0.8rem;
    border: 1px solid var(--hub-border, rgba(255, 255, 255, 0.08));
    border-radius: 4px;
    background: rgba(0, 0, 0, 0.15);
    color: inherit;
}

.cls-image-remove {
    background: none;
    border: none;
    color: #f87171;
    font-size: 1.2rem;
    cursor: pointer;
    padding: 0 0.3rem;
    line-height: 1;
}

.cls-image-remove:hover { color: #ef4444; }

.cls-image-drag-handle {
    position: absolute;
    top: 0.3rem;
    right: 0.4rem;
    font-size: 0.9rem;
    color: var(--hub-text-muted, #96ACBB);
    opacity: 0.5;
    cursor: grab;
}

/* ========================================
   Buttons
   ======================================== */
.pl-btn {
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.5rem 1.25rem;
    border: 1px solid transparent;
    border-radius: 6px;
    font-size: 0.875rem;
    font-weight: 600;
    line-height: 1.2;
    cursor: pointer;
    transition: background-color 0.15s ease;
    white-space: nowrap;
    min-height: 36px;
    text-decoration: none;
}

.pl-btn--primary {
    background: var(--color-tuscan-yellow, #EEB44B);
    color: #092E4C;
}

.pl-btn--primary:hover {
    background: #f5c86a;
}

.pl-btn--secondary {
    background: rgba(255, 255, 255, 0.08);
    color: var(--hub-text, #F4EFDD);
}

.pl-btn--secondary:hover {
    background: rgba(255, 255, 255, 0.12);
}

.pl-btn--danger {
    background: #dc2626;
    color: #fff;
}

.pl-btn--danger:hover {
    background: #ef4444;
}

.pl-btn--success {
    background: #2f8f4e;
    color: #fff;
}

.pl-btn--success:hover {
    background: #37a259;
}

/* Low-emphasis inline action, the pl-btn twin of .hub-btn--ghost. Nine wiki-moderation
   controls claimed this modifier while nothing defined it, so every "ghost" control
   rendered as a bare .pl-btn: no background, no colour, indistinguishable from the solid
   buttons beside it, and the graduated escalation the markup describes did not render.
   Same species as the .pl-input bug above, fixed the same way. */
.pl-btn--ghost {
    background: transparent;
    color: var(--hub-text-muted);
    border: 1px solid var(--hub-border-strong);
}

.pl-btn--ghost:hover {
    background: rgba(255, 255, 255, 0.06);
    color: var(--hub-text);
    border-color: var(--hub-text-muted);
}

/* A white wash over a light card is no wash at all. The house idiom for this is a
   [data-theme="light"] override (see .pl-confirm-warn), not color-mix, which appears
   nowhere in this stylesheet. */
[data-theme="light"] .pl-btn--ghost:hover {
    background: rgba(0, 0, 0, 0.05);
}

.pl-btn--sm {
    padding: 0.3rem 0.7rem;
    font-size: 0.8125rem;
    min-height: 0;
    border-radius: 4px;
}

/* Clears the field or toggle above a button, so a formset's Delete button never sits
   flush against the last input. Use this, never an inline margin-top. */
.pl-btn--spaced {
    margin-top: 0.75rem;
}

.pl-btn--icon {
    width: 36px;
    height: 36px;
    padding: 0;
    border-radius: 50%;
}

/* ========================================
   Color Picker
   ======================================== */
.pl-color-picker {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    background: var(--hub-input-bg, rgba(255, 255, 255, 0.06));
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 6px;
    padding: 0.375rem 0.5rem;
    width: fit-content;
    min-width: 160px;
}

.pl-color-picker:focus-within {
    border-color: var(--color-tuscan-yellow, #EEB44B);
    box-shadow: 0 0 0 3px rgba(238, 180, 75, 0.15);
}

.pl-color-picker__swatch-wrapper {
    position: relative;
    width: 28px;
    height: 28px;
    border-radius: 4px;
    overflow: hidden;
    border: 1px solid rgba(255, 255, 255, 0.1);
    flex-shrink: 0;
}

.pl-color-picker__input {
    position: absolute;
    top: -50%;
    left: -50%;
    width: 200%;
    height: 200%;
    cursor: pointer;
    border: none;
    padding: 0;
    background: none;
}

.pl-color-picker__hex {
    background: none !important;
    border: none !important;
    color: var(--hub-text, #F4EFDD) !important;
    font-family: monospace !important;
    font-size: 0.9375rem !important;
    padding: 0 !important;
    width: 80px !important;
    box-shadow: none !important;
}

.pl-color-picker__hex:focus {
    outline: none !important;
}

[data-theme="light"] .pl-color-picker {
    background: #fff;
    border-color: #d1d5db;
}

[data-theme="light"] .pl-color-picker__hex {
    color: #1f2937 !important;
}

/* ========================================
   Cart
   ======================================== */
.pl-cart {
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 8px;
    padding: 1.25rem;
    background: rgba(255, 255, 255, 0.02);
}

.pl-cart__title {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--hub-text-muted, #96ACBB);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin: 0 0 1rem;
}

.pl-cart__empty {
    font-size: 0.875rem;
    color: var(--hub-text-muted, #96ACBB);
}

.pl-cart__items {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin-bottom: 1rem;
}

.pl-cart__item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5rem 0;
    border-bottom: 1px solid rgba(255, 255, 255, 0.04);
    font-size: 0.875rem;
}

.pl-cart__item:last-child {
    border-bottom: none;
}

.pl-cart__item-name {
    flex: 1;
    color: var(--hub-text, #F4EFDD);
}

.pl-cart__item-qty {
    color: var(--hub-text-muted, #96ACBB);
    font-size: 0.8125rem;
}

.pl-cart__item-price {
    color: var(--hub-text, #F4EFDD);
    font-weight: 500;
    min-width: 60px;
    text-align: right;
}

.pl-cart__item-remove {
    background: none;
    border: none;
    color: var(--hub-text-muted, #96ACBB);
    cursor: pointer;
    padding: 0.25rem;
    font-size: 0.75rem;
}

.pl-cart__item-remove:hover {
    color: #f87171;
}

.pl-cart__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding-top: 0.75rem;
    border-top: 1px solid rgba(255, 255, 255, 0.08);
}

.pl-cart__total {
    font-size: 1rem;
    font-weight: 700;
    color: var(--hub-text, #F4EFDD);
}

/* ========================================
   Quantity picker (inside modal)
   ======================================== */
.pl-qty {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.pl-qty__btn {
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid rgba(255, 255, 255, 0.12);
    border-radius: 6px;
    background: rgba(255, 255, 255, 0.06);
    color: var(--hub-text, #F4EFDD);
    font-size: 1.125rem;
    cursor: pointer;
}

.pl-qty__btn:hover {
    background: rgba(255, 255, 255, 0.1);
}

.pl-qty__value {
    font-size: 1rem;
    font-weight: 600;
    color: var(--hub-text, #F4EFDD);
    min-width: 2rem;
    text-align: center;
}

/* ========================================
   Light theme overrides
   ======================================== */
[data-theme="light"] .pl-modal {
    background: #fff;
    border-color: #e5e7eb;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}

[data-theme="light"] .pl-modal__title {
    color: #1f2937;
}

[data-theme="light"] .pl-modal__header {
    border-color: #e5e7eb;
}

[data-theme="light"] .pl-modal__close {
    color: #6b7280;
}

[data-theme="light"] .pl-modal__close:hover {
    color: #1f2937;
}

[data-theme="light"] .pl-toggle__slider {
    background-color: #ccc;
}

[data-theme="light"] .pl-toggle-row {
    border-color: #e5e7eb;
}

[data-theme="light"] .pl-toggle-label {
    color: #1f2937;
}

[data-theme="light"] .pl-toggle-desc {
    color: #6b7280;
}

[data-theme="light"] .pl-form-label {
    color: #6b7280;
}

[data-theme="light"] .pl-form-group input,
[data-theme="light"] .pl-form-group select,
[data-theme="light"] .pl-form-group textarea {
    background: #fff;
    border-color: #d1d5db;
    color: #1f2937;
}

[data-theme="light"] .pl-form-group input[type="time"],
[data-theme="light"] .pl-form-group input[type="date"],
[data-theme="light"] .pl-form-group input[type="datetime-local"] {
    color-scheme: light;
}

[data-theme="light"] .pl-form-group input[type="file"]::file-selector-button {
    background: #f3f4f6;
    color: #1f2937;
    border-color: #d1d5db;
}

[data-theme="light"] .pl-field-hint {
    color: #6b7280;
}

[data-theme="light"] .pl-btn--secondary {
    background: #f3f4f6;
    color: #1f2937;
    border-color: #cbd0d6;
}

[data-theme="light"] .pl-btn--secondary:hover {
    background: #e5e7eb;
    border-color: #9ca3af;
}

[data-theme="light"] .pl-cart {
    border-color: #e5e7eb;
    background: #fafafa;
}

[data-theme="light"] .pl-cart__title {
    color: #6b7280;
}

[data-theme="light"] .pl-cart__item {
    border-color: #e5e7eb;
}

[data-theme="light"] .pl-cart__item-name {
    color: #1f2937;
}

[data-theme="light"] .pl-cart__item-qty {
    color: #6b7280;
}

[data-theme="light"] .pl-cart__item-price {
    color: #1f2937;
}

[data-theme="light"] .pl-cart__footer {
    border-color: #e5e7eb;
}

[data-theme="light"] .pl-cart__total {
    color: #1f2937;
}

[data-theme="light"] .pl-qty__btn {
    border-color: #d1d5db;
    background: #fff;
    color: #1f2937;
}

[data-theme="light"] .pl-qty__btn:hover {
    background: #f3f4f6;
}

[data-theme="light"] .pl-qty__value {
    color: #1f2937;
}

[data-theme="light"] .pl-cart__empty {
    color: #6b7280;
}

/* Screen-reader-only text (mirror of hub.css .sr-only) — pages that load only
   components.css (e.g. the Django admin override) still hide toggle captions. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ===== Social / link icons (profile contact rows) ===== */
.pl-social-icon {
    width: 1em;
    height: 1em;
    display: inline-block;
    vertical-align: middle;
    flex-shrink: 0;
}

/* ===== Refund UI (shared by the billing dashboard, CMS registration detail, teach portal) ===== */

/* Theme-aware status badge for payment / refund states. */
.pl-status-badge {
    display: inline-block;
    font-size: 0.75rem;
    padding: 0.2rem 0.5rem;
    border-radius: 4px;
    font-weight: 500;
    white-space: nowrap;
}
.pl-status-badge--ok { background: rgba(52, 211, 153, 0.15); color: #6ee7b7; }
.pl-status-badge--warn { background: rgba(251, 191, 36, 0.15); color: #fbbf24; }
.pl-status-badge--fail { background: rgba(220, 60, 60, 0.15); color: #f87171; }
.pl-status-badge--muted { background: rgba(255, 255, 255, 0.06); color: var(--hub-text-muted); }
[data-theme="light"] .pl-status-badge--ok { background: rgba(16, 150, 100, 0.12); color: #0b7a52; }
[data-theme="light"] .pl-status-badge--warn { background: rgba(180, 120, 10, 0.12); color: #8a5b06; }
[data-theme="light"] .pl-status-badge--fail { background: rgba(200, 50, 50, 0.1); color: #b32d2d; }
[data-theme="light"] .pl-status-badge--muted { background: rgba(0, 0, 0, 0.06); color: var(--hub-text-muted); }

/* ===== Roster & waitlist management (teach + admin Workspace tabs) ===== */

/* Filter-bar / inline-action control on admin pages (selects + text inputs).
   Theme input tokens per FRONTEND.md Rule 13 — never inline background/color. */
.pl-admin-control {
    padding: 0.45rem 0.75rem;
    border: 1px solid var(--hub-input-border, var(--hub-border));
    border-radius: 6px;
    background: var(--hub-input-bg);
    color: var(--hub-text);
    font-size: 0.875rem;
}
/* Native option popups don't inherit the select's colors (Rule 13). */
.pl-admin-control option {
    background: var(--hub-card-bg);
    color: var(--hub-text);
}

/* ========================================
   Row actions overflow menu (.pl-row-menu)
   Kebab trigger + fixed-position dropdown for table-row actions. The menu is
   position:fixed (placed from JS in components/row_actions.html) so it escapes
   the roster table wrapper's overflow:hidden clip. Both themes.
   ======================================== */
.pl-row-menu {
    display: inline-flex;
}

.pl-row-menu__trigger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    background: none;
    border: none;
    border-radius: 6px;
    color: var(--hub-text-muted);
    cursor: pointer;
    transition: color 0.15s ease, background-color 0.15s ease;
}

.pl-row-menu__trigger:hover,
.pl-row-menu__trigger:focus-visible {
    color: var(--hub-text);
    background-color: rgba(255, 255, 255, 0.06);
}

[data-theme="light"] .pl-row-menu__trigger:hover,
[data-theme="light"] .pl-row-menu__trigger:focus-visible {
    background-color: rgba(0, 0, 0, 0.06);
}

.pl-row-menu__menu {
    position: fixed;
    display: block;
    min-width: 200px;
    background-color: var(--hub-dropdown-bg);
    border: 1px solid var(--hub-dropdown-border);
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
    overflow: hidden;
    z-index: 150;
}

.pl-row-menu__item {
    display: block;
    width: 100%;
    padding: 0.625rem 1rem;
    font-size: 0.875rem;
    font-family: var(--font-body);
    color: var(--hub-text);
    background: none;
    border: none;
    cursor: pointer;
    text-align: left;
    text-decoration: none;
    transition: background-color 0.15s ease;
}

.pl-row-menu__item:hover {
    background-color: rgba(255, 255, 255, 0.05);
    color: var(--hub-text);
}

[data-theme="light"] .pl-row-menu__item:hover {
    background-color: rgba(0, 0, 0, 0.04);
}

.pl-row-menu__item:focus-visible {
    outline: 2px solid var(--hub-blue);
    outline-offset: -2px;
}

.pl-row-menu__item--danger { color: #e05555; }
.pl-row-menu__item--danger:hover { color: #e05555; }

.pl-row-menu__divider {
    height: 1px;
    background-color: var(--hub-dropdown-border);
    margin: 0.25rem 0;
}

@media (max-width: 768px) {
    .pl-row-menu__trigger {
        width: 2.75rem;
        height: 2.75rem;
    }
}

/* Muted sub-line under the Paid badge ("Link sent Aug 26" / "No email sent yet"). */
.pl-paid-note {
    font-size: 0.75rem;
    color: var(--hub-text-muted);
    margin-top: 0.2rem;
    white-space: nowrap;
}
.pl-paid-note--warn { color: #fbbf24; }
[data-theme="light"] .pl-paid-note--warn { color: #8a5b06; }

/* Warn-toned line inside a confirm modal (over-capacity, in-flight payment). */
.pl-confirm-warn {
    margin: 0 0 1rem;
    padding: 0.6rem 0.75rem;
    border-radius: 6px;
    font-size: 0.85rem;
    line-height: 1.45;
    background: rgba(251, 191, 36, 0.12);
    color: #fbbf24;
}
[data-theme="light"] .pl-confirm-warn {
    background: rgba(180, 120, 10, 0.1);
    color: #8a5b06;
}

/* The refund modal body — context header, consequence line, busy state. */
.pl-refund-context {
    margin: 0 0 1rem;
    padding-bottom: 0.75rem;
    border-bottom: 1px solid var(--hub-border, rgba(255, 255, 255, 0.08));
}
.pl-refund-context__payer {
    font-weight: 700;
    margin: 0 0 0.15rem;
}
.pl-refund-context__meta {
    font-size: 0.8125rem;
    color: var(--hub-text-muted);
    margin: 0;
}
.pl-refund-consequence {
    font-size: 0.8125rem;
    color: var(--hub-text-muted);
    margin: 1rem 0 0.75rem;
}
.pl-refund-failure {
    font-size: 0.8125rem;
    color: #f87171;
    margin: 0.5rem 0 0;
}
[data-theme="light"] .pl-refund-failure { color: #b32d2d; }
.pl-refund-form.htmx-request button[type="submit"] {
    opacity: 0.6;
    pointer-events: none;
}

/* Refund history rows (detail page card + panel amount sub-lines). */
.pl-refund-history {
    list-style: none;
    margin: 0.75rem 0 0;
    padding: 0;
    display: grid;
    gap: 0.5rem;
}
.pl-refund-history__row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem;
    font-size: 0.875rem;
}
.pl-refund-history__meta {
    color: var(--hub-text-muted);
    font-size: 0.8125rem;
}
.pl-refund-line {
    display: block;
    color: var(--hub-text-muted);
    font-size: 0.8125rem;
}

/* Payments ledger cells (the billing dashboard's dark-fixed partial — literal
   colors match the page's own pl-* chrome, like .pl-text-muted there). */
.pl-payer-link { color: #a0c0e0; }
.pl-cell-muted { color: #96ACBB; }
.pl-table-empty {
    text-align: center;
    color: #96ACBB;
    padding: 2rem;
}

/* "Signed In Devices" card on the settings Account tab — the phones that can sign a member
   in with a face or a fingerprint. The list itself is server rendered and shows on the web
   too, so a member can kill a lost phone's access from a laptop. Only the app-only block
   inside it is revealed by static/js/biometric-auth.js. */
.pl-biometric-devices {
    margin-bottom: 1.5rem;
}

/* The `hidden` attribute is only a UA-stylesheet `display:none`, so any author rule that
   sets display outranks it and the element stays on screen whatever the script does. That
   is real here: `.pl-btn` is `display:inline-flex`, so the "Turn on ..." button would show
   on every device. One rule for every descendant the script toggles. */
.pl-biometric-devices [hidden] {
    display: none;
}

.pl-biometric-devices__title {
    font-size: 1rem;
    font-weight: 600;
    margin: 0 0 0.25rem;
}

.pl-biometric-devices__intro {
    margin: 0 0 1rem;
    font-size: 0.85rem;
}

.pl-biometric-devices__list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.pl-biometric-devices__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 0.75rem 1rem;
    border: 1px solid var(--hub-border, rgba(255, 255, 255, 0.1));
    border-radius: 8px;
    background: var(--hub-surface, rgba(255, 255, 255, 0.04));
}

.pl-biometric-devices__row + .pl-biometric-devices__row {
    margin-top: 0.5rem;
}

.pl-biometric-devices__device {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;
}

.pl-biometric-devices__name {
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--hub-text, #F4EFDD);
    overflow-wrap: anywhere; /* device_label is member typed and can be one long word */
}

.pl-biometric-devices__meta {
    font-size: 0.8rem;
    color: var(--hub-text-muted, #8b97a8);
}

.pl-biometric-devices__empty {
    margin: 0;
    font-size: 0.85rem;
}

/* display lives here, not inline, so toggling [hidden] cannot strand a display:flex on it.
   The card-wide `.pl-biometric-devices [hidden]` rule above is what actually hides it. */
.pl-biometric-devices__app {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
    margin-top: 1.5rem;
}

.pl-biometric-devices__note {
    margin: 0;
    font-size: 0.85rem;
}


/* "Get the app" store badges (#467): templates/includes/app_store_badges.html, on the member
   home as a card and in the sidebar tray (the --tray modifier lives in hub.css). The block starts
   hidden and static/js/app-store-badges.js reveals it in a browser; the native app never sees it.
   [hidden] is an explicit rule so the flex rule below cannot outrank the attribute. The badge
   artwork is official and fixed colour: Apple's minimum is 40px tall, and Google's PNG carries
   its own clear space, so it is 52px tall to show the badge itself at 40px, level with Apple's. */
.pl-app-badges[hidden] {
    display: none;
}

.pl-app-badges {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.75rem;
}

/* On the home card the heading keeps the sibling cards' 1rem rhythm: its own margin plus the
   flex gap above add up to it, instead of stacking on top of it. */
.pl-app-badges .pl-home-heading {
    margin-bottom: 0.25rem;
}

.pl-app-badges__caption {
    margin: 0;
    font-size: 0.9375rem;
    color: var(--hub-text-muted, #8b97a8);
}

.pl-app-badges__links {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 0.75rem;
}

.pl-app-badges__link {
    display: inline-flex;
    line-height: 0;
}

.pl-app-badges__img {
    height: 40px;
    width: auto;
}

.pl-app-badges__img--play {
    height: 52px;
}

/* Leadership Directory role lines (#464, #476): one title and email row in a formset, on the
   admin member edit Details tab and on the Leadership Directory admin page alike. */
.pl-leadership-role {
    padding: 0.75rem;
    margin-bottom: 0.75rem;
    background: var(--hub-surface);
    border: 1px solid var(--hub-border);
    border-radius: 6px;
}

.pl-leadership-role__fields {
    display: flex;
    gap: 0.75rem;
    flex-wrap: wrap;
}

.pl-leadership-role__fields > div {
    flex: 1 1 220px;
    min-width: 0;
}

/* The formset's DELETE checkbox: driven by the real Delete button, never shown. */
.pl-leadership-role__flag {
    display: none;
}

/* Breathing room before the Save button below (FRONTEND.md Rule 18). */
.pl-leadership__add {
    margin: 0.25rem 0 1.5rem;
}
