/* ============================================================================
 * GP Shared State Kit — css/gp-state-kit.css
 * ----------------------------------------------------------------------------
 * The four states every fetching surface owes the member: LOADING (skeleton),
 * EMPTY, ERROR and OFFLINE. Built once here, in the frozen shared library, so
 * the per-page waves consume a state instead of reinventing one per surface.
 *
 * Slice: F6 of docs/plans/2026-07-26-app-native-ux.md ("F6. The shared state
 * kit"). Controller: js/gp-state-kit.js. Enqueued site-wide, both doors, by
 * inc/gp-state-kit.php.
 *
 * WHY THESE SHAPES (the plan's anti-slop rules, made concrete):
 *   - Rule 2: a skeleton that does not match the shape of the content it
 *     replaces is worse than no skeleton, because it creates a SECOND layout
 *     shift. So the variants below are content shapes (row / card / media /
 *     text / list), not one generic grey box.
 *   - Rule 3: "No results" is not an empty state. The empty block always has a
 *     mark, a line naming what would be here, and ONE action that fixes it.
 *   - Rule 6: a spinner is correct for an ACTION, never for a SCREEN. There is
 *     deliberately no screen spinner in this file; .gp-spinner in the frozen
 *     gp-core.css stays the in-button/in-action affordance.
 *   - Rule 9: a loading state that is a blank region is a bug. The skeleton
 *     reserves the space before the fetch starts.
 *
 * MOTION: shimmer is OPACITY ONLY. The usual background-position sweep across a
 * large gradient repaints the element every frame; an opacity pulse is a
 * compositor-only property, so a full list of skeletons costs the main thread
 * nothing (plan Section 4, "Motion adds 0ms of main-thread blocking time").
 * Under prefers-reduced-motion the pulse stops and the SKELETON STAYS, because
 * removing the placeholder would remove the information, not the motion.
 *
 * TOKENS: Tier-2/Tier-3 semantics only, per CLAUDE.md. Both themes are checked;
 * every token consumed here resolves under [data-theme="dark"] in gp-base.css.
 * One new semantic was added for this slice, --gp-color-skeleton (see gp-base).
 *
 * NAMING NOTE: the state block is .gp-state--empty / --error / --offline, NOT
 * .gp-empty-state. That class is already live with its own rules in
 * css/gp-recruiting-portal.css, and .gp-empty is already taken by gp-core.css.
 * One block with three modifiers also keeps the three states visually related,
 * which is what makes the error read as "the same screen, gone wrong."
 *
 * @package Gymnastics_Plus
 * @since 3.0.0
 * ============================================================================ */

/* ----------------------------------------------------------------------------
 * 1. SKELETON
 * ----------------------------------------------------------------------------
 * .gp-skeleton is the container; the greyed parts inside it are the __ elements.
 * Variants compose those parts into the shape of real content. Callers can also
 * hand-assemble parts inside a bare .gp-skeleton when a surface has a shape the
 * five variants do not cover, which is the point of exposing the primitives.
 *
 * A skeleton is decorative: mark the container aria-hidden and set aria-busy on
 * the region it fills. js/gp-state-kit.js does both automatically.
 * -------------------------------------------------------------------------- */

.gp-skeleton {
    display: block;
}

/* The one greyed primitive. Every part below is this plus a size. */
.gp-skeleton__block,
.gp-skeleton__line,
.gp-skeleton__avatar,
.gp-skeleton__thumb,
.gp-skeleton__media {
    display: block;
    background-color: var(--gp-color-skeleton);
    border-radius: var(--gp-r-xs);
    animation: gp-skeleton-pulse 1400ms var(--gp-motion-ease-in-out) infinite;
}

/* Opacity only. No background-position, no gradient, no layout property. */
@keyframes gp-skeleton-pulse {
    0%   { opacity: 1; }
    50%  { opacity: 0.45; }
    100% { opacity: 1; }
}

/* A small stagger between stacked lines reads as one breathing surface rather
   than a block of squares blinking in lockstep. Delay is free (no extra frames). */
.gp-skeleton__line + .gp-skeleton__line {
    animation-delay: 120ms;
}
.gp-skeleton__line + .gp-skeleton__line + .gp-skeleton__line {
    animation-delay: 240ms;
}

/* Text lines. A line of body copy is roughly 1em tall with its leading, so the
   placeholder is sized off the type ramp rather than a magic pixel value. */
.gp-skeleton__line {
    height: var(--gp-t-sub);
    border-radius: var(--gp-r-full);
}
.gp-skeleton__line + .gp-skeleton__line {
    margin-top: 0.5rem;
}
.gp-skeleton__line--title {
    height: var(--gp-t-md);
    width: 60%;
}
.gp-skeleton__line--short {
    width: 45%;
}

/* Round avatar, matching the 44pt minimum touch target list rows are built on. */
.gp-skeleton__avatar {
    width: 2.75rem;
    height: 2.75rem;
    flex: 0 0 auto;
    border-radius: var(--gp-r-full);
}

/* Square media thumbnail (list-row trailing image). */
.gp-skeleton__thumb {
    width: 4rem;
    height: 4rem;
    flex: 0 0 auto;
    border-radius: var(--gp-r-sm);
}

/* Wide media block. aspect-ratio keeps the reserved space correct at every
   width, which is what actually prevents the second layout shift. */
.gp-skeleton__media {
    width: 100%;
    aspect-ratio: 16 / 9;
    border-radius: var(--gp-r-sm);
}

/* -- variant: row (a list row) ------------------------------------------- */
.gp-skeleton--row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.75rem 0;
}
.gp-skeleton--row .gp-skeleton__lines {
    flex: 1 1 auto;
    min-width: 0;
}

/* -- variant: list (n rows, hairline separated, like a real list) --------- */
.gp-skeleton--list > .gp-skeleton--row {
    border-bottom: 1px solid var(--gp-color-border);
}
.gp-skeleton--list > .gp-skeleton--row:last-child {
    border-bottom: 0;
}

/* -- variant: card -------------------------------------------------------
 * Mirrors the frozen .gp-card (gp-core.css) exactly: same --gp-card-bg fill,
 * and the same resolved radius and elevation. It reads those from gp-base's
 * --gp-r-sm / --gp-e1 rather than gp-core's --gp-card-radius / --gp-card-shadow
 * aliases, because THOSE are declared in gp-core's own :root and gp-core is not
 * loaded site-wide, while this file is. The aliases resolve to exactly these two
 * tokens (gp-core.css lines 51 and 52), so the card skeleton and a real card are
 * the same shape on every route. This is consuming the library, not forking it:
 * no new radius, no new shadow, no new fill. */
.gp-skeleton--card {
    background: var(--gp-card-bg);
    border-radius: var(--gp-r-sm);
    box-shadow: var(--gp-e1);
    padding: 1rem;
}
.gp-skeleton--card .gp-skeleton__media {
    margin-bottom: 0.875rem;
}

/* -- variant: media (bare media block, no chrome) ------------------------- */
/* -- variant: text (a paragraph of copy) ---------------------------------- */
.gp-skeleton--text .gp-skeleton__line:last-child {
    width: 45%;
}

/* -- reduced motion: the pulse stops, the skeleton stays ------------------
 * gp-base.css already collapses every animation globally under this query, but
 * that leaves the element at whatever opacity the collapsed keyframe lands on.
 * Pin it explicitly so a reduced-motion member sees a steady, fully-opaque
 * placeholder rather than a faint one. */
@media (prefers-reduced-motion: reduce) {
    .gp-skeleton__block,
    .gp-skeleton__line,
    .gp-skeleton__avatar,
    .gp-skeleton__thumb,
    .gp-skeleton__media {
        animation: none;
        opacity: 1;
    }
}

/* ----------------------------------------------------------------------------
 * 2. STATE BLOCK — empty, error, offline
 * ----------------------------------------------------------------------------
 * One shape, three modifiers. All three are: a mark, a title naming what would
 * be here, one line of guidance, and at most ONE action (plan anti-slop rule 10,
 * two competing primary actions on one screen is a bug).
 *
 * The three are told apart by the MARK and the COPY, not by a flood of colour.
 * Podium principle 2: red is signal, never decoration, so the error state gets a
 * red hairline ring and a red glyph, not a red panel.
 * -------------------------------------------------------------------------- */

.gp-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 0.5rem;
    padding: 3rem 1.5rem;
    animation: gp-state-in var(--gp-motion-duration-fast) var(--gp-motion-ease-out) both;
}

/* Enter on transform + opacity only, so it composites. */
@keyframes gp-state-in {
    from { opacity: 0; transform: translateY(0.5rem); }
    to   { opacity: 1; transform: translateY(0); }
}

/* The mark: a neutral tile with a monochrome glyph. Sized as a real focal
   element (56px) rather than the oversized 64px icon the legacy empty states
   use, which reads as clip art at phone width. */
.gp-state__mark {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 3.5rem;
    height: 3.5rem;
    margin-bottom: 0.25rem;
    /* border-STRONG, not border. Verified on the harness: the tile fill is one
       elevation step off the surface behind it, which reads clearly on a dark
       card but is almost nothing on a white one, so the ring is what actually
       draws the mark in light mode and the hairline weight was too faint for
       that job. Strong reads in both and stays calm in dark. */
    border: 1px solid var(--gp-color-border-strong);
    border-radius: var(--gp-r-full);
    background: var(--gp-color-surface-2);
    color: var(--gp-color-text-muted);
}
.gp-state__mark svg {
    width: 1.5rem;
    height: 1.5rem;
    display: block;
}

.gp-state__title {
    margin: 0;
    font-size: var(--gp-t-body);
    font-weight: 600;
    line-height: 1.3;
    color: var(--gp-color-text);
}

.gp-state__body {
    margin: 0;
    max-width: 34ch;
    font-size: var(--gp-t-sub);
    line-height: 1.45;
    color: var(--gp-color-text-muted);
}

/* The action is a real .gp-btn from the frozen library (gp-core.css) — this
   file never restyles its fill, only its placement and its touch target. On the
   handful of routes that do not load gp-core the button degrades to a plain
   browser button: still labelled, still focusable, still wired to the retry
   callback. */
.gp-state__action {
    margin-top: 0.75rem;
    min-height: 2.75rem;
}

/* -- error: same shape, red signal ---------------------------------------- */
.gp-state--error .gp-state__mark {
    border-color: var(--gp-color-danger);
    color: var(--gp-color-danger);
}

/* -- offline: same shape, a dashed ring reading "disconnected" ------------
 * Distinguished by the ring STYLE, the glyph and the copy, not by a third
 * accent colour. Amber would have been the obvious pick and is the wrong one:
 * --gp-color-warning is 2.15:1 on a white surface, which fails even the 3:1
 * non-text minimum, so it cannot carry a mark in light mode. */
.gp-state--offline .gp-state__mark {
    border-style: dashed;
}

/* Compact variant, for a state that has to sit inside a card or a small panel
   rather than owning the screen. */
.gp-state--compact {
    padding: 1.75rem 1rem;
}
.gp-state--compact .gp-state__mark {
    width: 2.75rem;
    height: 2.75rem;
}
.gp-state--compact .gp-state__mark svg {
    width: 1.25rem;
    height: 1.25rem;
}

/* ----------------------------------------------------------------------------
 * 3. OFFLINE BANNER
 * ----------------------------------------------------------------------------
 * The app door is a WebView with the service worker deliberately suppressed
 * (inc/gp-app-bootstrap.php section 5b), so losing signal is a real, reachable
 * state and the member deserves to be told once, quietly, rather than watching
 * taps do nothing. js/gp-state-kit.js mounts exactly one of these per document.
 *
 * Per DESIGN.md principle 5 it stays dark in BOTH themes, so its white ink is
 * always legible. It reuses --gp-color-fill-ink, whose own token comment already
 * records that it deliberately has no dark redefinition: the fill stays ink in
 * both themes. Ink plus --gp-nav-ink white is 16.6:1.
 *
 * DELIBERATELY NOT GLASS, despite being chrome. The first build frosted it with
 * the always-dark glass tokens and the harness caught the problem: over a light
 * canvas, rgba(20,22,28,.58) plus brightness(.86) composites to roughly a
 * mid-grey, which puts 13px white text at about 4.2:1 and fails AA. The same
 * tokens are fine on the tab bar because that carries icons at the 3:1 graphic
 * threshold, not small text. A solid pill also removes the WebView glass
 * degradation question entirely, and per DESIGN.md the motion is the signature,
 * not the glass. The glass BORDER and SHADOW are kept: a faint rim-light and a
 * lifted shadow are correct on a solid dark pill.
 * -------------------------------------------------------------------------- */

.gp-offline-banner {
    position: fixed;
    /* Centred with symmetric insets plus auto margins, NOT left:50% plus
       translateX(-50%). With left:50% and no right, a shrink-to-fit box can only
       ever be half the viewport wide, so the label wrapped onto three lines at
       phone width. Insetting both edges gives the pill the full width to size
       itself in, and keeps the transform free for the enter motion alone. */
    left: 1rem;
    right: 1rem;
    width: -moz-fit-content;
    width: fit-content;
    margin-left: auto;
    margin-right: auto;
    bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
    z-index: 9998; /* above the 9997 bottom nav, below the 9999 mobile header */
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.625rem 1rem;
    border: 1px solid var(--gp-glass-border-dark);
    border-radius: var(--gp-r-full);
    background: var(--gp-color-fill-ink); /* always-ink pill fill; no dark override by design */
    box-shadow: var(--gp-glass-shadow-dark);
    color: var(--gp-nav-ink);             /* always white; not theme-aware, by design */
    font-size: var(--gp-t-foot);
    font-weight: 500;
    line-height: 1.3;
    text-align: left;
    /* The pill is content-width up to max-width, so it stays on one line at
       every phone width we support and only wraps on something narrower. It is
       deliberately allowed to WRAP rather than ellipsize: truncating a status
       message to "You are offline. Check your con..." is worse than a pill two
       lines tall. */
    /* Hidden resting state. visibility (not display) so the transform/opacity
       transition has something to animate from. */
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transform: translateY(0.5rem);
    transition: opacity var(--gp-motion-duration-fast) var(--gp-motion-ease-out),
                transform var(--gp-motion-duration-fast) var(--gp-motion-ease-out),
                visibility 0s linear var(--gp-motion-duration-fast);
}

.gp-offline-banner.is-visible {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
    transition: opacity var(--gp-motion-duration-fast) var(--gp-motion-ease-out),
                transform var(--gp-motion-duration-fast) var(--gp-motion-ease-out),
                visibility 0s;
}

/* Clear the bottom nav where the bottom nav actually exists. 768px matches the
   nav chrome breakpoint in css/gp-mobile-core.css. --gp-bottom-nav-height is
   declared by that same stylesheet, so the 0px fallback is exactly right on the
   pages where no bottom nav is enqueued. */
@media (max-width: 768px) {
    .gp-offline-banner {
        bottom: calc(var(--gp-bottom-nav-height, 0px) + env(safe-area-inset-bottom, 0px) + 1rem);
    }
}

/* Status is a coloured dot AND a word, never colour alone (accessibility rule
   in DESIGN.md, colour-blind safe). The word is the banner text. */
.gp-offline-banner__dot {
    width: 0.5rem;
    height: 0.5rem;
    flex: 0 0 auto;
    border-radius: var(--gp-r-full);
    background: var(--gp-color-warning);
}

.gp-offline-banner--online .gp-offline-banner__dot {
    background: var(--gp-color-success);
}

.gp-offline-banner__text {
    min-width: 0;
}

@media (prefers-reduced-motion: reduce) {
    .gp-state {
        animation: none;
    }
    .gp-offline-banner {
        transition: none;
    }
}
