

/* ============================================================
   Visual system – components
   Cards, forms, buttons, dialogs, toasts, and the other shared
   component styles. Selectors are the PHP/JS class names.
   ============================================================ */

/* Component identities formerly supplied by generic utility classes. */
.Notice {
    color: var(--muted);
}
.PageGenerationTime,
.UsernameAvailability { font-size: 0.875rem; }
.UsernameAvailability.Available { color: var(--muted); }
.PageGenerationTime { color: var(--ink); }
.HiddenLabel {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.ErrorList,
.VerificationNotice {
    display: flex;
    flex-direction: column;
}
.ErrorList { gap: 0.5rem; }
.VerificationNotice { gap: 1rem; }
.VerificationNoticeActions,
.SitePolicyLinks { display: flex; }
.VerificationNoticeActions { gap: 0.5rem; }
.SitePolicyLinks { gap: 1rem; }
.NavAccount {
    display: flex;
    gap: 1rem;
    margin-inline-start: auto;
}

.PageTitle,
.ListSection > h2,
.NavDropdownMenu :is(a, .LogoutButton) {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* ---------- Cards & surfaces ---------- */

/* The shared surfaces used by MossArch pages. */
:is(
    .Card, .Form, .SearchBox, .ThemeSelector, .NavDropdownMenu,
    .ConfirmDialogCard, .TestResults, .TestSuitePanel
) {
    background-color: var(--surface);
    background-image: var(--surface-image, none);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-card);
    padding: 1rem;
    box-shadow: var(--shadow-card);
    overflow: hidden;
}

.Card { box-shadow: var(--shadow-pop); }

/* Prose is the one thing that still spaces itself with margins, so a surface
   trims the leaked margin off its first and last child rather than asking
   prose to behave like components. Keep the two lists matching the one above. */
:is(
    .Card, .Form, .SearchBox, .ThemeSelector, .NavDropdownMenu,
    .ConfirmDialogCard, .TestResults, .TestSuitePanel
) > *:last-child {
    margin-bottom: 0;
}

:is(
    .Card, .Form, .SearchBox, .ThemeSelector, .NavDropdownMenu,
    .ConfirmDialogCard, .TestResults, .TestSuitePanel
) > *:first-child {
    margin-top: 0;
}

/* Every form stacks what it asks for; the submit button's own align-self is
   what puts it at the trailing edge. */
.Form,
.ThemeSelector,
.LanguageSelector,
.TestSuitePanel {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.TestSuitePanel { align-items: flex-start; }

.NavDropdownMenu {
    max-height: 90vh;
    overflow-y: auto;
}

/* ---------- Animations ---------- */

@keyframes spin {
    to { transform: rotate(360deg); }
}

@keyframes mount-in {
    from { opacity: 0; transform: translateY(0.5rem); }
    to   { opacity: 1; transform: translateY(0); }
}

.LoadingSpinner {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 5rem;
    padding: 1rem;
    border-radius: 1rem;
}

.LoadingSpinner {
    background: linear-gradient(100deg, transparent, var(--surface-2), transparent);
    background-size: 200% 100%;
    animation: state-shimmer 1.8s ease-in-out infinite;
}

.LoadingSpinner::after {
    content: '';
    width: 2.25rem;
    height: 2.25rem;
    border: 3px solid var(--hairline);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.6s linear infinite;
}

@keyframes state-shimmer { to { background-position: -200% 0; } }

.MountIn {
    animation: mount-in 0.25s ease;
}

.SlidingOut {
    overflow: hidden;
    opacity: 0;
    transition: height 0.2s ease, opacity 0.2s ease;
}

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

/* ---------- A refused field ---------- */

/* Under the input it is about, in the colour everything that has gone wrong
   wears. Tied to the input by aria-describedby, so it is read out on reaching
   the box rather than found by hunting - which is the whole point of putting
   it here instead of in a toast floating at the edge of the screen. */
.FieldError {
    color: var(--danger);
    font-size: 0.8125rem;
}

/* One question a form asks: label over control over the reason it was
   refused, spaced by the field itself. */
.InputField,
.TextareaField {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

/* The box itself, so the eye lands on which one before reading why. */
.InputField :is(input, textarea)[aria-invalid='true'],
.TextareaField :is(input, textarea)[aria-invalid='true'] {
    border-color: var(--danger);
    outline-color: var(--danger);
}

/* ---------- Skipping the navigation ---------- */

/* Out of the way until somebody tabs to it, then a real control in the corner.
   Moved off-screen rather than hidden: display:none and visibility:hidden both
   take it out of the tab order, which is the one thing it must stay in. */
.SkipLink {
    position: absolute;
    top: 0;
    inset-inline-start: 0;
    z-index: 1000;
    padding: 1rem;
    background-color: var(--surface);
    color: var(--ink);
    border: 1px solid var(--hairline);
    border-start-start-radius: 0;
    border-end-end-radius: 0;
    border-end-start-radius: var(--radius-card);
    transform: translateY(-120%);
}

.SkipLink:focus {
    transform: translateY(0);
}

/* The content it lands on is only focusable so the jump lands - it is not a
   control, and should not draw a ring as though it were. */
#PageContent:focus {
    outline: none;
}

/* ---------- Buttons ---------- */

.Button {
    background-color: var(--surface-2);
    background-image: var(--button-image, none);
    color: var(--ink) !important;
    border: none;
    border-radius: var(--radius-pill);
    padding: 0.5rem 1rem;
    font-weight: 550;
    font-size: 0.875rem;
    line-height: 1.4;
    cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease, transform 0.1s ease, opacity 0.15s ease;
}

.Button:hover {
    background-color: var(--surface);
    /* Falls back to the resting image, so a theme with a gradient but no hover
       variant simply keeps the one it has rather than losing it on hover. */
    background-image: var(--button-image-hover, var(--button-image, none));
    color: var(--ink) !important;
}

.Button:active { transform: scale(0.97); }
.Button:disabled { opacity: 0.55; cursor: default; transform: none; }

/* Waiting on the server. Disabling alone only dims a control slightly and
   then nothing happens, which on anything slow reads as a press that did not
   land - so it pulses for as long as it is working. The animation beats the
   :disabled opacity above by being an animation, which is the whole reason
   this is not just another opacity rule. */
@keyframes throb {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.4; }
}

.Working {
    animation: throb 1.1s ease-in-out infinite;
    cursor: progress;
}

/* Somebody who has asked for less movement still needs to know it is working,
   so the pulse becomes a steady dimming rather than nothing at all. */
@media (prefers-reduced-motion: reduce) {
    .Working { animation: none; opacity: 0.5; }
}

/* Forms lay their fields out in a column, and the button that submits one sits
   at the trailing edge rather than stretching across it. */
.SubmitButton { align-self: flex-end; }

/* A checkbox or radio with a label wrapped round it is a control you press, so
   it takes the button's own colours and hover and keeps the box inside. The
   wrapping is what makes the whole of it the target rather than the box alone,
   so this only claims the ones built that way. */
label:has(> input[type='checkbox']),
label:has(> input[type='radio']) {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    background-color: var(--surface-2);
    background-image: var(--button-image, none);
    color: var(--ink);
    border-radius: var(--radius-pill);
    padding: 0.5rem 1rem;
    margin-bottom: 0;
    font-weight: 550;
    font-size: 0.875rem;
    line-height: 1.4;
    cursor: pointer;
    user-select: none;
    transition: background-color 0.15s ease, color 0.15s ease;
}

label:has(> input[type='checkbox']):hover,
label:has(> input[type='radio']):hover {
    background-color: var(--surface);
    background-image: var(--button-image-hover, var(--button-image, none));
    color: var(--ink);
}

a.Button {
    display: inline-block;
    text-align: center;
}

/* Author styles override the browser's default [hidden] rule, so keep a
   fallback link genuinely out of the layout until its scroller needs it. */
a.Button[hidden] { display: none; }

a.Button:hover { color: var(--ink) !important; }

/* ---------- Forms (component classes) ---------- */

.CheckboxField {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.CheckboxField input { width: auto; margin: 0; }
.CheckboxField label { display: inline; }

.TextareaField textarea {
    width: 100%;
    resize: vertical;
}

/* Every wording the button can show, stacked in one cell so the widest of
   them sets the width once and toggling never resizes it. */
.ToggleButton {
    display: inline-grid;
    grid-template-areas: 'label';
    place-items: center;
}

.ToggleButtonLabel { grid-area: label; }

.ToggleButtonLabel.Inactive { visibility: hidden; }

.ProgressBar {
    display: none;
    width: 100%;
    height: 0.4rem;
    border-radius: var(--radius-pill);
    overflow: hidden;
    accent-color: var(--accent);
}

.ProgressBar.Active { display: block; }

.User {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
}

/* ---------- Search ---------- */

.SearchBox {
    position: relative;
    display: flex;
    align-items: center;
    padding: 0.5rem;
}


.SearchInput {
    width: 100%;
    border: none;
    background-color: transparent;
    font-size: 1rem;
}

.SearchInput:focus { box-shadow: none; }
.SearchInput:focus-visible {
    outline: none !important;
    box-shadow: none !important;
}

/* .SearchInput itself is borderless and flush with .SearchBox, so the
   keyboard focus ring belongs on the box a reader actually sees. */
.SearchBox:focus-within {
    outline: 2px solid var(--theme-stop-3);
    outline-offset: 0;
    box-shadow: 0 0 0 4px var(--theme-stop-2);
}

.SearchClearButton {
    display: none;
    flex: none;
    padding: 0 0.5rem;
    border: none;
    background: none;
    color: var(--danger) !important;
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}

.SearchBox.HasQuery .SearchClearButton { display: block; }

/* The words for the language offer, rendered by the server because they are in
   a language this page is not in. LanguagePrompt.js takes them into a dialog
   and takes this away; it is never something to read in place. */
.LanguagePrompt { display: none; }

/* ---------- Confirm dialog ---------- */

.ConfirmDialogOverlay {
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    backdrop-filter: blur(1rem);
    z-index: 40;
}

body:has(.ConfirmDialogOverlay) {
    overflow: hidden;
}

.ConfirmDialogCard {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    width: min(24rem, 100%);
    box-shadow: var(--shadow-pop);
}

.ConfirmDialogMessage {
    color: var(--ink);
}

.ConfirmDialogInput {
    resize: vertical;
}

/* Action rows put the main choice last, at the trailing edge. */
.DialogActions,
.ConfirmDialogActions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.5rem;
}

/* Shaped like every other button on the site: the same pill, and no stretching
   to the width of the row they sit in. */
.DialogActions .Button {
    flex: none;
    border-radius: var(--radius-pill);
}

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

/* The card stacks the badge over the output with its own gap. */
.TestResults {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.TestResultsBadge {
    display: inline-block;
    align-self: flex-start;
    padding: 0.5rem;
    border-radius: var(--radius-ctl);
    font-weight: 600;
}

.TestResultsPass {
    background-color: var(--accent-soft);
    color: var(--accent-strong);
}

.TestResultsFail {
    background-color: var(--danger-soft);
    color: var(--danger);
}

.TestResultsOutput {
    margin: 0;
    padding: 0.5rem;
    border-radius: var(--radius-ctl);
    background-color: var(--surface-2);
    color: var(--ink);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.8125rem;
    line-height: 1.5;
    white-space: pre-wrap;
    overflow-x: auto;
}

/* ---------- Error & error list ---------- */

.Error {
    padding: 1rem 1.25rem;
    color: var(--danger);
    border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent);
    border-radius: 1rem;
    background: var(--danger-soft);
}

.ErrorList {
    background-color: var(--danger-soft);
    border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
    border-radius: var(--radius-ctl);
    padding: 0.5rem 1rem;
    list-style: none;
    margin: 0;
    color: var(--danger);
}

/* ---------- Settings sections (fold‑outs) ---------- */

.SettingsSection {
    border-top: 1px solid var(--hairline);
}

.SettingsSection:last-of-type {
    border-bottom: 1px solid var(--hairline);
}

/* The body, when open: its own padding is internal to its card, so nothing
   otherwise pushes its outer edge away from whatever divider follows. */
.SettingsSection > *:last-child {
    margin-bottom: 1rem;
}

.SettingsSection > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    cursor: pointer;
    padding-block: 1rem;
    list-style: none;
    font-family: var(--display-font);
    font-weight: 640;
    font-size: 1.2rem;
    line-height: 1.2;
    letter-spacing: -0.01em;
}

.SettingsSection > summary::-webkit-details-marker {
    display: none;
}

.SettingsSection > summary:hover {
    color: var(--accent);
}

.SettingsSection > summary::after {
    content: '';
    flex: none;
    width: 0.5rem;
    height: 0.5rem;
    border-right: 2px solid var(--muted);
    border-bottom: 2px solid var(--muted);
    transform: rotate(45deg);
    transition: transform 0.15s ease;
}

.SettingsSection[open] > summary::after {
    transform: rotate(-135deg);
}

/* ---------- Toasts ---------- */
.ToastContainer {
    position: fixed;
    bottom: 1rem;
    inset-inline-end: 1rem;
    z-index: 2001;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    width: min(22rem, calc(100vw - 2rem));
}

.Toast {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    background-color: var(--surface);
    border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
    border-radius: var(--radius-card);
    box-shadow: var(--shadow-pop);
    padding: 0.5rem 1rem;
    color: var(--ink);
    font-size: 0.875rem;
    opacity: 0;
    transform: translateY(0.5rem);
    transition: opacity 0.2s ease, transform 0.2s ease;
}

.Toast.Active {
    opacity: 1;
    transform: translateY(0);
}

.ToastMessage {
    flex: 1 1 auto;
}

.ToastCloseButton {
    background: none;
    border: none;
    padding: 0;
    line-height: 1;
    color: var(--muted);
    cursor: pointer;
    transition: color 0.15s ease;
}

.ToastCloseButton:hover {
    color: var(--ink);
}

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