/* Copyright (c) 2025 Onur Gumus. All Rights Reserved.
   Licensed under the proprietary license. See LICENSE file in the project root. */

/* ===========================================================================
   polish.css - cross-cutting fixes that must beat the per-page stylesheets.

   Why a separate file rather than additions to styles.css: six of the eleven
   stylesheets (roadmaps, lesson, exam, stats, admin, quick-course - about 6,580
   lines) declare no @layer at all, and an unlayered normal rule beats every
   layered normal rule regardless of specificity. Anything added to styles.css's
   @layer base is therefore silently dead on those six pages. This file is
   unlayered and injected last, so it wins on load order without !important.

   Its one hard limit: styles.css has 178 !important declarations inside
   @layer overrides, and for !important the layer order REVERSES - layered
   !important beats unlayered !important. So .hamburger, .mobile-menu-container
   and the @scope (nav) internals cannot be reached from here at any specificity.
   Those have to be edited in that layer directly.
   =========================================================================== */

/* ============================================================ focus rings

   The app had ZERO :focus-visible rules and 26 `outline: none` declarations.
   Keyboard navigation was therefore completely invisible: tabbing through the
   app moved focus with nothing on screen to say where it went. That is not a
   nicety - it makes the app unusable without a mouse, and it is the single
   worst accessibility defect in the codebase.

   :where() keeps specificity at zero so any real component style still wins on
   colour, while the `outline` itself is restored because none of those 26 rules
   are !important. Applied to :focus-visible only, so it appears for keyboard
   and programmatic focus but not on mouse clicks - which is why the original
   `outline: none` rules were added in the first place. */

:where(a, button, input, select, textarea, summary, [tabindex], [contenteditable="true"]):focus-visible {
    outline: 2px solid var(--cyan, #22d3ee);
    outline-offset: 2px;
    /* A cyan ring on a cyan button is invisible; the shadow guarantees a
       light/dark boundary against whatever sits underneath. */
    box-shadow: 0 0 0 4px color-mix(in oklch, var(--bg-primary, #0d0d0d) 80%, transparent);
    border-radius: inherit;
}

/* Custom controls that are focusable but not natively so. */
:where([role="button"], [role="tab"], [role="menuitem"], [role="option"]):focus-visible {
    outline: 2px solid var(--cyan, #22d3ee);
    outline-offset: 2px;
}

/* Skipping the ring on the element that received focus purely because a dialog
   opened would be wrong, so nothing is excluded here. */

/* ============================================================ tap targets

   Under a coarse pointer, anything below ~44px is a coin-flip to hit. This
   grows the hit area without changing layout: the ::after box is invisible and
   absolutely positioned, so the button keeps its rendered size.

   NOTE: .hamburger is 40x40 and is the ONLY nav affordance on mobile, but it
   lives under the layered !important block described above and cannot be fixed
   from here - see styles.css @layer overrides. */

@media (pointer: coarse) {
    :where(button, a.btn, .btn, [role="button"]):not(.hamburger) {
        min-block-size: 44px;
    }

    /* Text links that act as navigation. Measured at 360px: .back-link was
       26px tall - the primary "get out of here" control on the roadmap page. */
    :where(.back-link, .nav-link, .breadcrumb a) {
        display: inline-flex;
        align-items: center;
        min-block-size: 44px;
    }

    /* The AI teacher panel's own controls, measured at 360px: the launcher was
       134x38, the provider select 176x28, and the header icons 25-40px wide.
       Sized rather than given ::after hit areas because they sit in tight rows
       where an invisible 44px box would overlap its neighbour. */
    .teacher-chat-btn,
    .teacher-panel__mode-select {
        min-block-size: 44px;
    }

    .teacher-panel__lang-toggle,
    .teacher-panel__minimize,
    .teacher-panel__close,
    .teacher-action-btn {
        min-inline-size: 44px;
        min-block-size: 44px;
    }

    /* Footer links measured 19px tall at 360px - inline text with no padding,
       four of them in a row. They are low-traffic, but a 19px target is a
       coin-flip on a phone. Padding rather than min-block-size, so they keep
       flowing as text instead of becoming blocks. */
    footer a,
    .learn-empty__aside a {
        display: inline-block;
        padding-block: 0.7rem;
    }

    /* Icon-only controls are the ones that are actually too small; give them a
       centred 44px hit area regardless of their painted size. The close buttons
       are listed by name because measuring found them at 40x20. */
    :where(.icon-btn, .btn-icon-only, .ui-toast__close, .learn-tabs__help,
           .login-dialog-close, .modal-close, .close-btn, .tour-skip) {
        position: relative;
    }
    :where(.icon-btn, .btn-icon-only, .ui-toast__close, .learn-tabs__help,
           .login-dialog-close, .modal-close, .close-btn, .tour-skip)::after {
        content: "";
        position: absolute;
        inset-block: 50%;
        inset-inline: 50%;
        inline-size: 44px;
        block-size: 44px;
        translate: -50% -50%;
    }
}

/* ============================================================ inputs

   Below 16px, iOS Safari zooms the viewport when a field takes focus and never
   zooms back out - the page is left scaled and horizontally scrollable. This is
   the single most common "the mobile site is broken" report. */

@media (width <= 768px) {
    /* Deliberately NOT :where() and deliberately !important. Measured on a
       360px device: #commentTextInput was 15.2px and the two teacher-panel
       selects were 11.52px, all set by ordinary page rules that outrank a
       zero-specificity selector. This is a floor that prevents a platform
       bug, not a design opinion, so it has to actually win. Unlayered
       !important still loses only to the layered !important in styles.css,
       which targets nav internals, never form fields. */
    input,
    select,
    textarea {
        font-size: max(16px, 1rem) !important;
    }
}

/* ============================================================ dvh

   100vh on mobile is the viewport with the URL bar HIDDEN, so a full-height
   element is taller than the visible area until you scroll. 100dvh tracks the
   real one. Applied only where a full-height box is the intent. */

:where(.modal-overlay, .node-details-panel, .lesson-modal) {
    max-block-size: 100dvh;
}

/* ============================================================ reduced motion

   study.css has a correct global reduced-motion block, but it only ships on the
   seven pages that load study.css. The landing page - 27 animations and 25
   keyframes - dashboard, roadmaps, course-overview, stats and admin got nothing.
   For people who set this because motion makes them ill, "most of the app"
   is not an implementation.

   Transitions are collapsed rather than removed so state changes still land;
   `animation: none` would leave anything that animates INTO position stuck at
   its start frame, so those are reduced to a single instant iteration. */

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    /* Purely decorative loops have nothing to land on - stop them outright. */
    :where(.progress-spinner, .loading-spinner, .pulse, .float, .shimmer) {
        animation: none !important;
    }
}

/* ============================================================ overflow guard

   Any single wide child - a long code block, a wide table, an over-wide grid -
   makes the whole page scroll sideways, which on a phone reads as the layout
   being broken. Containing it to the element that is actually too wide. */

:where(pre, table, .table-wrapper, .code-block) {
    max-inline-size: 100%;
    overflow-x: auto;
}

/* ============================================================ card system

   Five card-ish components each picked their own geometry independently, and
   .stat-card was defined TWICE with different values - 16px radius and a -5px
   hover lift on the dashboard, 12px and -2px on stats. Same component name,
   two looks, depending which page you were on.

     .course-card      20px  translateY(-5px)  + backdrop-filter: blur(20px)
     .stat-card (dash) 16px  translateY(-5px)  + backdrop-filter: blur(20px)
     .stat-card (stats)12px  translateY(-2px)
     .roadmap-card     12px  translateY(-2px)
     .spine-node       12px  scale(1.03)

   NOTE ON SPECIFICITY. The first attempt wrapped these in :where() and did
   nothing at all - measured .stat-card still at 12px - because :where() is
   specificity zero and every one of the rules above is at least 0,1,0. Being
   the last-loaded sheet only breaks ties at EQUAL specificity. So these use
   plain class selectors, and the hover rules restate the page selectors
   exactly (including `:not(.add-course)`, which makes that one 0,3,0). */

:root {
    --r-card: 16px;
    --lift: -4px;
    --e-hover: 0 8px 24px oklch(0 0 0 / 0.18);
}

.course-card,
.stat-card,
.roadmap-card,
.spine-node,
.sample-flashcard {
    border-radius: var(--r-card);
}

/* One lift for all of them. These set `transform` rather than the independent
   `translate` property because that is what the page rules set - leaving those
   in place and adding a translate on top would compose to a double lift. */
.course-card:hover:not(.add-course),
.stat-card:hover,
.roadmap-card:hover,
.sample-flashcard:hover {
    transform: translateY(var(--lift));
    box-shadow: var(--e-hover);
}

/* The spine node scaled instead of lifting, which made the roadmap tree feel
   unlike every other card in the app. */
.spine-node:hover {
    transform: translateY(var(--lift));
    box-shadow: var(--e-hover);
}

@media (prefers-reduced-motion: reduce) {
    .course-card:hover:not(.add-course),
    .stat-card:hover,
    .roadmap-card:hover,
    .spine-node:hover,
    .sample-flashcard:hover {
        transform: none;
    }
}

/* backdrop-filter forces the compositor to sample and blur everything beneath
   each card. On a dashboard that is 20+ blurred layers being recomputed while
   scrolling - and the cards sit on an opaque background, so the blur is not
   even visible. Keep it where it reads, on the translucent nav. */
@media (pointer: coarse) {
    .course-card,
    .stat-card {
        backdrop-filter: none;
    }
}
