/*
 * ZERG3RRBOT accessibility and polish layer.
 *
 * Load order: this file must be linked AFTER tailwind.min.css. It only adds
 * keyboard-focus, reduced-motion, contrast and tap-target behaviour, and it is
 * written so that mouse users on a default-motion system see no difference.
 *
 * The Bootstrap-era rules that used to live here were removed: no template
 * linked the file, and several of them (`.mb-3`, `.mb-4`, `.mt-4` with
 * !important, `canvas`, `h1, h2, h3`, `body`, `main`) collide with Tailwind
 * class names and would have shifted every page the moment it was linked.
 *
 * tests/test_a11y_polish_css.py parses this file and recomputes every contrast
 * ratio from the values below, so lowering one fails the suite.
 */

:root {
    /* 2px ring: 15.0:1 or better on the four dark surfaces, 6.2:1 on the purple button. */
    --a11y-focus-ring: #FFFFFF;
    --a11y-focus-ring-width: 2px;
    --a11y-focus-ring-offset: 2px;

    /* Muted text. Was #848494 in tailwind.config.js (4.47:1 on a card, 4.09:1
       on an alt card). #9696A6 is 5.16:1 or better on every solid surface and
       4.61:1 or better on the 10% tinted cards, and still sits clearly below
       secondary text (#ADADB8). */
    --a11y-text-muted: #9696A6;
    --a11y-text-muted-rgb: 150 150 166;

    /* Purple as TEXT only. The brand purple (#8245AA in tailwind.config.js) is
       2.1:1 to 3.1:1 as text on the dark surfaces. #B685DB is 5.28:1 or better
       on the four solid surfaces and 4.56:1 or better on the 10% and 20%
       purple chips over them. Backgrounds, borders, rings and buttons keep
       the brand value. The hover value is one step lighter again. */
    --text-purple: #B685DB;
    --text-purple-rgb: 182 133 219;
    --text-purple-hover: #CDA6EC;
    --text-purple-hover-rgb: 205 166 236;

    /* Green BEHIND white text only. White on the success green (#00C853 in
       tailwind.config.js) is 2.24:1. #008037 is 5.07:1 and its hover value
       5.94:1. Tinted chips, green text, borders and the on state of a switch
       keep the brand green. */
    --success-solid: #008037;
    --success-solid-rgb: 0 128 55;
    --success-solid-hover: #007230;
    --success-solid-hover-rgb: 0 114 48;

    --a11y-tap-size: 44px;
    /* Floor for small in-flow controls (footer links, widget picker, notice
       dismiss): the box grows to this, so only use it where there is room. */
    --a11y-tap-min: 44px;
}

/* ---------------------------------------------------------------------------
 * 1. Keyboard focus
 *
 * Tailwind emits `.focus\:outline-none:focus` (specificity 0,2,0), which hides
 * the outline for keyboard users as well. Each selector below is prefixed with
 * `:root`, giving 0,2,1 for element selectors and 0,3,0 for attribute
 * selectors, so it beats the utility in either source order without
 * !important. `:focus-visible` keeps the ring off mouse clicks on buttons and
 * links. Browsers always match `:focus-visible` on text-entry fields, so those
 * show the ring on click too; that is by design.
 *
 * tabindex="-1" is left out: those are script-focused containers (dialog
 * panels, skip targets), and ringing a whole panel is noise.
 * ------------------------------------------------------------------------- */
:root a:focus-visible,
:root button:focus-visible,
:root input:focus-visible,
:root select:focus-visible,
:root textarea:focus-visible,
:root summary:focus-visible,
:root [role="button"]:focus-visible,
:root [role="tab"]:focus-visible,
:root [tabindex]:not([tabindex="-1"]):focus-visible {
    outline: var(--a11y-focus-ring-width) solid var(--a11y-focus-ring);
    outline-offset: var(--a11y-focus-ring-offset);
}

/* Elements that draw their own Tailwind `focus:ring-*` would show two rings.
   Under keyboard focus the outline above is the single indicator, so the
   ring layers are zeroed (the element's ordinary `shadow-*` is untouched).
   Mouse focus still gets the element's own ring exactly as before. */
:root [class*="focus:ring-"]:focus-visible {
    --tw-ring-shadow: 0 0 #0000;
    --tw-ring-offset-shadow: 0 0 #0000;
}

/* Visually hidden checkbox or radio driving a styled sibling (the toggle
   switch pattern): the 1px input cannot show a ring, so its track does. */
:root input.sr-only:focus-visible + * {
    outline: var(--a11y-focus-ring-width) solid var(--a11y-focus-ring);
    outline-offset: var(--a11y-focus-ring-offset);
}

/* ---------------------------------------------------------------------------
 * 2. Reduced motion
 *
 * Durations are collapsed rather than animations removed, so state changes
 * still land (instantly) and `transitionend` / `animationend` still fire,
 * which Alpine `x-transition` and `x-collapse` wait for. !important is
 * required here: Alpine and several templates set transition and animation
 * values as inline styles, which no selector can outrank.
 *
 * OBS overlay pages are excluded twice over: they do not link this file, and
 * the rule skips any document whose root carries `.obs-overlay`. If an overlay
 * template ever links this stylesheet, add class="obs-overlay" to its <html>.
 * ------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
    :root:not(.obs-overlay) *,
    :root:not(.obs-overlay) *::before,
    :root:not(.obs-overlay) *::after {
        animation-duration: 0.01ms !important;
        animation-delay: 0s !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        transition-delay: 0s !important;
        scroll-behavior: auto !important;
    }

    /* Loading spinners are exempt. Collapsed to one instant turn, a spinner
       stops mid-frame and reads as a broken icon, so the one sign that a
       request is still running would be lost. It keeps turning, at half
       speed. Progress bars need no exemption: they move by changing width,
       which still lands, only without the tween. */
    :root:not(.obs-overlay) .animate-spin {
        animation-duration: 2s !important;
        animation-iteration-count: infinite !important;
    }
}

/* ---------------------------------------------------------------------------
 * 3. Contrast: muted text
 *
 * tailwind.min.css hard-codes the colour (`rgb(132 132 148 / ...)`), so the
 * token cannot be changed through a variable. This overrides the utilities by
 * name, at the same specificity, relying on this file loading later.
 *
 * The `:where(:not(...))` guard lists every colour utility that Tailwind emits
 * AFTER `.text-text-muted`. Those currently win when an element carries both
 * classes (for example an Alpine `:class` adding `text-white` to an active
 * tab); without the guard this later rule would start beating them. The test
 * rebuilds that list from tailwind.min.css and fails if one is missing.
 *
 * Durable fix: set `text.muted` to #9696A6 in tailwind.config.js, rebuild,
 * then delete this block.
 * ------------------------------------------------------------------------- */
.text-text-muted:where(:not(
    .text-text-muted\/30, .text-text-muted\/50, .text-text-muted\/60,
    .text-text-primary, .text-text-secondary,
    .text-twitch-dark, .text-twitch-dark\/30, .text-twitch-dark\/50, .text-twitch-dark\/70,
    .text-twitch-purple, .text-twitch-purple-light,
    .text-white, .text-white\/30, .text-white\/50, .text-white\/70,
    .text-yellow-300, .text-yellow-400, .text-yellow-500
)) {
    color: rgb(var(--a11y-text-muted-rgb) / var(--tw-text-opacity, 1));
}

.placeholder\:text-text-muted::placeholder {
    color: rgb(var(--a11y-text-muted-rgb) / var(--tw-text-opacity, 1));
}

/* ---------------------------------------------------------------------------
 * 4. Contrast: purple text
 *
 * Same method as section 3: tailwind.min.css hard-codes `rgb(130 69 170 / ...)`
 * for the text utilities, so they are overridden by name at the same
 * specificity, relying on this file loading later. Only `color` is touched;
 * `bg-twitch-purple`, `border-twitch-purple`, `ring-twitch-purple` and the
 * white-on-purple buttons are the brand colour and are not changed here.
 *
 * The guards list the colour utilities Tailwind emits AFTER each purple one,
 * which currently win when an element carries both (an Alpine `:class` adding
 * `text-white` to the active tab). tests/test_final_sweep_a11y.py rebuilds the
 * list from tailwind.min.css.
 *
 * The lighter and darker hover utilities both resolve to one lighter hover
 * value: the darker brand shade is under 2.3:1 on every surface.
 *
 * Durable fix: once the brand purple is decided, give Tailwind a text colour
 * that passes, rebuild, then delete this block.
 * ------------------------------------------------------------------------- */
.text-twitch-purple:where(:not(
    .text-white, .text-white\/30, .text-white\/50, .text-white\/70,
    .text-yellow-300, .text-yellow-400, .text-yellow-500
)),
.text-twitch-purple-light:where(:not(
    .text-white, .text-white\/30, .text-white\/50, .text-white\/70,
    .text-yellow-300, .text-yellow-400, .text-yellow-500
)) {
    color: rgb(var(--text-purple-rgb) / var(--tw-text-opacity, 1));
}

.hover\:text-twitch-purple:where(:not(.hover\:text-white)):hover,
.group:hover .group-hover\:text-twitch-purple {
    color: rgb(var(--text-purple-rgb) / var(--tw-text-opacity, 1));
}

.hover\:text-twitch-purple-light:where(:not(.hover\:text-white)):hover,
.hover\:text-twitch-purple-dark:where(:not(.hover\:text-white)):hover {
    color: rgb(var(--text-purple-hover-rgb) / var(--tw-text-opacity, 1));
}

/* White text on the success green: every solid green button, badge and the
   success toast change together here. Two classes (0,2,0) beat the utility
   (0,1,0) in either source order; the hover rule (0,4,0) beats
   `.hover\:bg-status-success\/80:hover` (0,2,0), which is lighter than the
   resting colour and would fail again. */
.bg-status-success.text-white {
    background-color: rgb(var(--success-solid-rgb) / var(--tw-bg-opacity, 1));
}

.bg-status-success\/90.text-white {
    background-color: rgb(var(--success-solid-rgb) / 0.9);
}

.bg-status-success.text-white.hover\:bg-status-success\/80:hover {
    background-color: rgb(var(--success-solid-hover-rgb) / var(--tw-bg-opacity, 1));
}

/* ---------------------------------------------------------------------------
 * 5. Tap targets
 * ------------------------------------------------------------------------- */

/* Opt-in helper: the element itself becomes at least 44 by 44. This does
   change the element's box, so use it where the layout has room. */
.tap-target {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--a11y-tap-size);
    min-height: var(--a11y-tap-size);
}

/* Small in-flow controls that an audit measured under 40px. Each grows to at
   least 40 by 40 and gives the extra height back through a negative margin,
   so the row it sits in keeps its height. */

/* Dismiss button on a one-line notice (the trial-ended notice under the nav):
   was 24 by 24. */
.notice-dismiss {
    display: inline-flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    min-width: var(--a11y-tap-min);
    min-height: var(--a11y-tap-min);
    margin: -8px -8px -8px 0;
}

/* "Add" in the dashboard widget picker: was 46 by 24. */
.widget-picker-add {
    display: inline-flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    min-width: 48px;
    min-height: var(--a11y-tap-min);
    margin-block: -6px;
}

/* Footer text links. base_new.html already makes each a 44px row on touch
   screens and narrow windows. On a wide window with a mouse the line stays
   compact: the link box grows to 40 by 40 through padding, and the matching
   negative margin keeps the footer the same height. */
@media (min-width: 1024px) and (pointer: fine), (min-width: 1024px) and (pointer: none) {
    .footer-link {
        display: inline-block;
        min-width: var(--a11y-tap-min);
        min-height: var(--a11y-tap-min);
        padding: 12px 4px;
        margin: -12px -4px;
        text-align: center;
    }
}

/* Touch devices only: a control whose only child element is an icon, inside
   the navbar, a dialog, a full-screen modal layer or a `.tap-scope` container,
   gets an invisible centred hit area of at least 44 by 44. The box, and so the
   layout, is unchanged. Everything sits in :where() (zero specificity) so any
   Tailwind positioning utility on the control still wins over the
   `position: relative` that anchors the hit area. */
@media (pointer: coarse) {
    :where(nav, header, dialog, [role="dialog"], [aria-modal="true"], .fixed.inset-0, .tap-scope)
    :where(button, a, [role="button"]):where(:has(> :is(svg, i[data-lucide]):only-child)) {
        position: relative;
    }

    :where(nav, header, dialog, [role="dialog"], [aria-modal="true"], .fixed.inset-0, .tap-scope)
    :where(button, a, [role="button"]):where(:has(> :is(svg, i[data-lucide]):only-child))::after {
        content: "";
        position: absolute;
        top: 50%;
        left: 50%;
        width: max(100%, var(--a11y-tap-size));
        height: max(100%, var(--a11y-tap-size));
        transform: translate(-50%, -50%);
    }
}

/* ---------------------------------------------------------------------------
 * 6. Toasts above the cookie bar
 *
 * Toasts are fixed to the bottom right and the cookie bar to the bottom edge,
 * at the same z-index, so the bar used to cover a toast. The bar publishes its
 * height as --consent-bar-height (0px once a choice is saved) and the toast
 * stack sits that much higher. Flash messages render in the page flow under
 * the navigation, so the bar never reaches them.
 * ------------------------------------------------------------------------- */
:root [x-data="toastContainer()"] {
    bottom: calc(1rem + var(--consent-bar-height, 0px));
}

/* ---------------------------------------------------------------------------
 * 7. Dialogs clear of the cookie bar
 *
 * A dialog layer (`fixed inset-0`) and the cookie bar share a z-index and the
 * bar comes later in the page, so on a short phone the bar covered a dialog's
 * last row of buttons. While the bar shows, every full-screen layer ends at
 * the bar's top edge instead of the bottom of the window: the dialog centres
 * and scrolls in the space that is left, and the cookie choice stays usable.
 * --consent-bar-height is 0px once a choice is saved, so nothing moves then.
 * A native <dialog> opened with showModal() is in the top layer and is
 * already above the bar.
 * ------------------------------------------------------------------------- */
.fixed.inset-0 {
    bottom: var(--consent-bar-height, 0px);
}

/* A dialog from the modal() macro never grows past the space left above the
 * cookie bar. Its title stays at the top, its text scrolls inside the panel,
 * and the row marked .modal-actions sticks to the bottom of that scrolling
 * area, so Cancel and the confirm button are on screen however long the text
 * is or however short the phone. */
.modal-panel {
    display: flex;
    flex-direction: column;
    max-height: calc(100vh - var(--consent-bar-height, 0px) - 2rem);
    max-height: calc(100dvh - var(--consent-bar-height, 0px) - 2rem);
}
.modal-panel > .modal-header {
    flex-shrink: 0;
}
.modal-panel > .modal-body {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
}
.modal-body .modal-actions {
    position: sticky;
    bottom: -1rem;
    margin-bottom: -1rem;
    padding-bottom: 1rem;
}

/* Every phone control has a 40px hit area, including compact utilities. */
@media (max-width: 639px) {
    :root button,
    :root select,
    :root textarea,
    :root input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not(.sr-only),
    :root summary,
    :root [role="button"],
    :root [role="tab"],
    :root a[href] {
        min-height: var(--a11y-tap-min);
    }
    /* A default, not an override: :where() gives this no specificity, so a
       link that something hides (.hidden, the trial chip under 640px, a
       [hidden] attribute's own rule below) stays hidden, and a link with a
       display of its own keeps it. Written with ordinary specificity it beat
       every one of those, and hidden links appeared on phones. */
    :where(:root a[href]:not(.sr-only):not(.flex):not(.block):not(.nav-link):not(.mobile-nav-link):not(.dropdown-item):not(.tools-item)) {
        display: inline-flex;
        align-items: center;
    }
    :root a[href][hidden] { display: none; }
    :root button:has(> :is(svg, i[data-lucide]):only-child),
    :root .notice-dismiss {
        min-width: var(--a11y-tap-min);
    }
    /* Preserve navigation's existing 44px floor. */
    :root nav a[href],
    :root nav button { min-height: var(--a11y-tap-size); }
    :root input[type="range"] { height: var(--a11y-tap-min); }
}
