/* =============================================================================
   Rise Foundation Payments - design system

   Typography and palette follow risefoundation.am, the foundation's own site.
   That resemblance is not cosmetic: a payment page looking unconnected to the
   site someone just left is indistinguishable from a phishing redirect.

   Fonts are self-hosted and subset in wwwroot/fonts - no CDN fonts or scripts on
   a page that takes card details, since every extra origin is another party who
   could alter it. The brand faces used to be merely NAMED in the stack and never
   shipped, which meant Armenian silently fell through to whatever the operating
   system happened to provide.

   Colours are role-named (--brand-500, --ink-700) rather than named after the
   hue, so a repalette is a token change and no component rule moves. Every value
   that carries text has its contrast ratio recorded beside it; the buttons on
   this site spend money, and an unreadable one is not a styling nicety.

   Dark mode overrides tokens only, never component rules, so the two themes
   cannot drift apart.
   ============================================================================= */

/* ---- fonts -------------------------------------------------------------------------------------
   Self-hosted and subset, from the same origin as everything else. No third-party font host is
   reachable from this page; see the no-CDN note above. Families match risefoundation.am so the
   payment surface is recognisably the same organisation.

   Each face declares a unicode-range, so the browser picks a file by CODEPOINT rather than by family
   order. Two consequences worth knowing: Armenian text can never be rendered in a Latin-only face
   however the stack is ordered or however <html lang> is tagged, and a page with no Armenian on it
   never downloads the Armenian files at all.

   Build steps and the reasons behind the subset are in wwwroot/fonts/README.md. */

@font-face {
  font-family: "Noto Sans"; src: url("fonts/noto-sans-latin.woff2") format("woff2");
  font-weight: 400 700; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Noto Sans Armenian"; src: url("fonts/noto-sans-armenian.woff2") format("woff2");
  font-weight: 400 700; font-style: normal; font-display: swap;
  unicode-range: U+0530-058F, U+FB13-FB17;
}
@font-face {
  font-family: "Source Serif 4"; src: url("fonts/source-serif-4-latin.woff2") format("woff2");
  font-weight: 400 700; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Noto Serif Armenian"; src: url("fonts/noto-serif-armenian.woff2") format("woff2");
  font-weight: 400 700; font-style: normal; font-display: swap;
  unicode-range: U+0530-058F, U+FB13-FB17;
}

:root {
    /*  Navy, from the foundation's logo. Sampled off wwwroot/favicon.png, which is exactly three
        colours on white: navy #005271, teal #2a9d8f, crimson #ee4266.

        --brand-500 used to be #2440a8, an indigo that appears NOWHERE in the logo. It was synthesised,
        which is why it was the only value in this file with no provenance line beside it, and it is
        almost certainly the shape of a fix for a real problem: navy is the only logo colour that can
        legally carry white button text. Measured against white, the teal gives 3.32:1 and the crimson
        3.75:1 - both under the 4.5:1 that the 15px button label needs - while the navy gives 8.59:1.
        So the interactive rungs are the logo navy and a darker navy, and the ramp is re-derived from it
        rather than having one value swapped: hue 196.5 to 198 the whole way up, which is also the hue
        family the dark block has always used. The two themes were different hues until this change.

          --brand-500 #005271  logo navy. White label 8.59:1; as a focus ring 8.59:1 on --surface and
                               7.29:1 on --page-bg, both over the 3:1 WCAG 1.4.11 asks of a control.
          --brand-600 #003f57  the hover, 7.8 L* darker so a press is visible. White label 11.36:1.
                               Also carries TEXT - a:hover, .filters__clear, .choice__go - at 11.36:1
                               on white, so it is held to 4.5:1 and not just to the 3:1 of a fill.

        --brand-700 is deliberately NOT the logo navy: it is still risefoundation.am's blue-navy, it
        colours every link and heading, and moving it is a far larger change than the buttons. It means
        the site currently carries two navies - see the note on it below.

        The 50/100/200 rungs are tints and borders and carry no text of their own; they were re-hued
        with the rest so the ramp is one family, at the same lightness they had, so nothing changed
        weight. --brand-700 on --brand-50 is 14.22:1. */
    --brand-50:  #eef5f8;
    --brand-100: #ddecf2;
    --brand-200: #b9d8e4;
    --brand-300: #3aa8a0;   /* the logo teal, lightened - 2.88:1 on white, so BORDERS AND TINTS ONLY, never behind text */
    --brand-500: #005271;   /* interactive: buttons, focus rings - white label 8.59:1 */
    --brand-600: #003f57;   /* hover, and link hover - white label 11.36:1 */
    --brand-700: #16204a;   /* risefoundation.am navy, NOT the logo navy - links and headings on white, 15.68:1 */

    /*  The donate button, and nothing else. Still deliberately outside the brand ramp - its job is to
        not read as one more navy button, which is why it was asked for - and now that is the REASON for
        the colour rather than a coincidence of it: #9a2b1e is risefoundation.am's --color-flag, the
        stamp red that site already reserves for giving. One giving colour across the foundation, no new
        token, and 169.8 degrees of hue away from --brand-500, so it cannot be mistaken for the primary.

        The brick also holds up as TEXT, which is what settled it against the crimson: risefoundation.am
        uses --color-flag for drop caps and honour-roll notes on its #eaedec paper, where #9a2b1e is
        6.51:1 and the crimson #cf2149 measured 4.49:1 - a hundredth short of AA, on eight usages.

        Do not reach for the raw logo crimson #ee4266 here: 3.75:1 behind a white label and 3.18:1 as
        text on the paper. It fails both roles, which is why the logo pink is a tint colour only.
          --donate-500 #9a2b1e, white label  7.67:1
          --donate-600 #7c2318, white label  9.95:1 (the hover, 7.1 L* darker) */
    --donate-500: #9a2b1e;
    --donate-600: #7c2318;

    /* Warm gold, shared with bebras.am's --color-accent. Highlights and value
       flags only - never a primary action, so "attention" and "the thing to
       click" stay visually distinct. */
    --accent-50:  #fdf3df;
    --accent-200: #f7dfa8;
    --accent-500: #f0a81e;
    --accent-700: #5a2c05;   /* ink on the amber flag - 5.7:1. Never lighten this, in either theme. */

    --ink-900: #1c2226;
    --ink-700: #39434a;
    --ink-500: #4f585e;
    --ink-400: #767f84;
    --ink-200: #d2d8d7;
    --ink-100: #dfe4e3;
    --ink-50:  #f2f4f3;
    --white:   #ffffff;

    --success-50:  #eaf5f2;
    --success-200: #a9d8cd;
    --success-700: #0f6f5c;

    --warn-50:  #fdf4e6;
    --warn-200: #f7dfa8;
    --warn-700: #8e4708;

    --danger-50:  #fbeceb;
    --danger-200: #f3bfbd;
    /* Logo pink #ee4266 gives only 3.75:1 with white, so the button tone is a shade
       deeper; the lighter pink stays for borders and tinted panels. */
    --danger-500: #d3282c;
    --danger-700: #a11418;

    --page-bg: #eaedec;   /* risefoundation.am --background */

    /* The admin sidebar's own ground. It used to be var(--text), which is near-white in dark mode -
       so the panel rendered white-on-white and was unreadable. Navy also makes it the most direct
       cue to risefoundation.am anywhere in the product. White label: 15.7:1 light, 17.5:1 dark. */
    --admin-shell-bg: #16204a;
    --surface: var(--white);
    --border: var(--ink-200);

    /*  The boundary of anything you can operate.

        --border is a divider: it separates a card from the page, a row from the next row, and being
        quiet is its whole job. Reused as the EDGE OF A CONTROL it is 1.44:1 light and 1.28:1 dark,
        against the 3:1 that WCAG 1.4.11 asks of a user-interface component boundary - so
        .btn--secondary and the stepper buttons had no perceivable edge at all and read as text that
        happened to be indented. This is the same family, dark enough to be an edge: 3.1:1 light. */
    --border-control: var(--ink-400);
    --text: var(--ink-900);
    --text-muted: var(--ink-500);

    --radius-sm: 6px;
    --radius: 10px;
    --radius-lg: 16px;

    --shadow-sm: 0 1px 2px rgba(15, 23, 42, .06);
    --shadow: 0 1px 3px rgba(15, 23, 42, .08), 0 4px 12px rgba(15, 23, 42, .05);
    --shadow-lg: 0 8px 30px rgba(15, 23, 42, .10);

    --space-1: .25rem;
    --space-2: .5rem;
    --space-3: .75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;
    --space-8: 3rem;

    --ff-sans-am: "Noto Sans Armenian";
    --ff-sans: "Noto Sans";
    --ff-serif-am: "Noto Serif Armenian";
    --ff-serif: "Source Serif 4";
    --ff-fb-sans: "Segoe UI", "Arial Armenian", system-ui, -apple-system, sans-serif;
    --ff-fb-serif: Georgia, "Times New Roman", serif;

    /* Armenian first, matching risefoundation.am. Ordering is cosmetic rather than functional -
       unicode-range already decides which file serves which character - but it settles the codepoints
       both families could draw, such as digits and punctuation. */
    --font-sans: var(--ff-sans-am), var(--ff-sans), var(--ff-fb-sans);
    --font-serif: var(--ff-serif-am), var(--ff-serif), var(--ff-fb-serif);

    /* Kept so nothing referencing --font breaks while the switch-over happens in the next commit. */
    --font: var(--font-sans);

    /* Display face for headings, as used across bebras.am and codzilla.am. Falls
       back to the body stack, so a missing file costs weight, not legibility. */
    --font-mono: "Cascadia Mono", "SF Mono", Consolas, "Liberation Mono", monospace;

    --content-width: 68rem;
    --narrow-width: 44rem;
}

/* Latin-first when the element is tagged en or ru, Armenian-first otherwise - as risefoundation.am
   does. Custom properties inherit, so <span lang="en">Bebras</span> inside Armenian prose picks this
   up without a rule of its own. */
:lang(en), :lang(ru) {
  --font-sans: var(--ff-sans), var(--ff-sans-am), var(--ff-fb-sans);
  --font-serif: var(--ff-serif), var(--ff-serif-am), var(--ff-fb-serif);
}


*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    font-family: var(--font-sans);
    font-size: 1rem;
    line-height: 1.6;
    color: var(--text);
    background: var(--page-bg);
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

/* ---------- Typography ---------------------------------------------------- */

/* Headings in the serif, body and forms in the sans. risefoundation.am sets serif for BODY copy too,
   which suits an editorial site; here the body is 15px form labels, table cells and 13px hints, and a
   serif at those sizes is materially harder to scan. Same identity, different register.

   The old -.01em tracking was tuned for Inter and a serif does not want it, and 650 is heavy on Source
   Serif, so both are reset here rather than inherited. */
h1, h2, h3, h4, .card__title, .product__name, .offer__name, .choice__title, .empty__title {
  font-family: var(--font-serif);
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1.3;
}
h1, h2, h3, h4 { margin: 0 0 var(--space-3); }
h1 { font-size: 1.875rem; }
h2 { font-size: 1.375rem; margin-top: var(--space-6); }
h3 { font-size: 1.125rem; margin-top: var(--space-5); }
h1:focus, h2:focus { outline: none; }

p { margin: 0 0 var(--space-4); }
p:last-child { margin-bottom: 0; }

a { color: var(--brand-700); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--brand-600); }

small, .text-sm { font-size: .875rem; }
.text-muted { color: var(--text-muted); }
.text-danger { color: var(--danger-700); }
code { font-family: var(--font-mono); font-size: .8125em; word-break: break-all; }

/* ---------- Layout -------------------------------------------------------- */

.page { flex: 1 0 auto; width: 100%; max-width: var(--content-width); margin: 0 auto; padding: var(--space-6) var(--space-4) var(--space-8); }

/* Used both as a modifier on .page and as a standalone wrapper inside it, so it has to centre itself. */
.page--narrow { max-width: var(--narrow-width); margin-inline: auto; }

/* Visible to screen readers only - for labels that would be redundant on screen. */
.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;
}

.site-header { background: var(--surface); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 50; }
.site-header__inner { max-width: var(--content-width); margin: 0 auto; padding: var(--space-3) var(--space-4); display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; }
.site-header__brand { font-weight: 700; color: var(--text); text-decoration: none; display: flex; align-items: center; gap: var(--space-2); margin-inline-end: auto; }
.site-header__mark { width: 2rem; height: 2rem; border-radius: var(--radius-sm); flex: none; object-fit: contain; }
.site-header__name { display: flex; flex-direction: column; line-height: 1.2; }
.site-header__name span:last-child { font-size: .75rem; font-weight: 500; color: var(--text-muted); }

.site-nav { display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap; }
.site-nav a { padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); color: var(--ink-700); text-decoration: none; font-size: .9375rem; font-weight: 500; }
.site-nav a:hover { background: var(--ink-100); color: var(--text); }
.site-nav a.active { background: var(--brand-50); color: var(--brand-700); }

/* The right-hand group: things reached for at any moment, kept together and away from the two
   destination links on the left. */
.site-header__actions { display: flex; align-items: center; gap: var(--space-2); }

/*  Icon-only, so it needs a real hit area rather than the size of the glyph: 2.5rem square is the
    smallest comfortable tap target, and an icon in a header is exactly where a too-small one hurts. */
.site-header__basket {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: var(--radius-sm);
    color: var(--ink-700);
    text-decoration: none;
}

.site-header__basket:hover { background: var(--ink-100); color: var(--text); }
.site-header__basket.active { background: var(--brand-50); color: var(--brand-700); }
.site-header__basket svg { width: 1.25rem; height: 1.25rem; }

/*  Slightly tighter than a page button - it is chrome, not a call to action on the page itself - and
    its own colour, so it does not read as one more navy button in a row of navy.

    Selected as .btn.site-header__donate, not .site-header__donate. Both this rule and .btn--primary are
    single-class selectors, and .btn--primary is declared further down the file, so an equal-specificity
    rule here would lose the cascade and the button would stay navy for no visible reason. */
.site-header__donate { padding: .4375rem var(--space-3); font-size: .9375rem; }
.btn.site-header__donate { background: var(--donate-500); color: var(--surface); }
.btn.site-header__donate:hover:not(:disabled) { background: var(--donate-600); color: var(--surface); }
.btn.site-header__donate:focus-visible { outline-color: var(--donate-500); }

@media (max-width: 30rem) {
    /*  On a narrow phone the donate label is the first thing to give way: the basket and the language
        switcher are navigation a visitor may need at any point, while supporting the foundation has its
        own page reachable from the footer. */
    /*  Matched on .btn as well, because the element carries both classes and .btn's own
        `display: inline-flex` is declared LATER in this file. Two single-class selectors of equal
        specificity are settled by source order, so this rule lost every time and the donate button
        stayed on screen at every width - the one responsive rule in the header, doing nothing. */
    .btn.site-header__donate { display: none; }
}

.site-footer { flex: none; background: var(--surface); border-top: 1px solid var(--border); padding: var(--space-6) var(--space-4); font-size: .875rem; color: var(--text-muted); }
.site-footer__inner { max-width: var(--content-width); margin: 0 auto; display: flex; flex-wrap: wrap; gap: var(--space-4) var(--space-6); justify-content: space-between; }
.site-footer nav { display: flex; flex-wrap: wrap; gap: var(--space-3); }

/* Full width under the columns: it applies to every page, so it reads as a standing statement rather
   than one more footer column. */
.site-footer__assurance { flex: 1 0 100%; padding-top: var(--space-4); border-top: 1px solid var(--border); max-width: 70ch; }

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

.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-5); box-shadow: var(--shadow-sm); }
.card + .card { margin-top: var(--space-4); }

/*  A card laid out BY A GRID must not space itself: the grid's own `gap` already did it, and a second
    helping of spacing on one axis only is not spacing, it is a misalignment.

    This is the defect behind "3 cards, they are not aligned properly". .card + .card matched every shop
    card after the first, so the second and third cards on /shop started 16px lower than the first - a
    ragged top edge across the row - and because a grid row stretches to its tallest item, the first card
    came out 16px TALLER than its neighbours to make up the difference. Measured at 1440px before this
    rule: tops 284.78 / 300.78 / 300.78, heights 885.39 / 869.39 / 869.39. After it, and with the cover
    box corrected further down: tops all 284.78, heights all 845.11.

    Scoped to grid children rather than removed, because stacked cards in normal flow - which is most of
    the admin panel - still need the margin; there is no gap to do it for them there. */
.grid > .card + .card { margin-top: 0; }

.card__title { margin: 0 0 var(--space-1); font-size: 1.0625rem; }
.card__hint { margin: 0 0 var(--space-4); color: var(--text-muted); font-size: .875rem; }

.grid { display: grid; gap: var(--space-4); }

/*  auto-fill with 1fr tracks gives every card in a row the same WIDTH; align-items: stretch gives every
    card in a row the same HEIGHT, whatever is inside it - a long Armenian title, a comparison price, an
    out-of-stock badge - because the row is sized by its tallest card and each card is then stretched to
    it. stretch is also the initial value; it is written down because the equal-height promise on this
    page rests on it, and on .product__foot's auto margin having a definite height to push against. */
.grid--products { grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); align-items: stretch; }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }

.stack > * + * { margin-top: var(--space-4); }
.row { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center; }
.row--end { justify-content: flex-end; }
.row--between { justify-content: space-between; }

/* ---------- Page header --------------------------------------------------- */

.page-header { margin-bottom: var(--space-5); }
.page-header__lead { color: var(--text-muted); max-width: 60ch; margin: 0; }
.breadcrumb { display: flex; gap: var(--space-2); font-size: .875rem; color: var(--text-muted); margin-bottom: var(--space-2); padding: 0; list-style: none; flex-wrap: wrap; }
.breadcrumb a { color: var(--text-muted); }

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

.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
    padding: .5625rem var(--space-4); border-radius: var(--radius-sm); border: 1px solid transparent;
    font: inherit; font-size: .9375rem; font-weight: 600; line-height: 1.2; cursor: pointer;
    text-decoration: none; transition: background-color .12s ease, border-color .12s ease, color .12s ease;
}
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }

.btn--primary { background: var(--brand-500); color: var(--surface); }
.btn--primary:hover:not(:disabled) { background: var(--brand-600); color: var(--surface); }

.btn--secondary { background: var(--surface); color: var(--ink-700); border-color: var(--border-control); }
.btn--secondary:hover:not(:disabled) { background: var(--ink-100); color: var(--text); }

.btn--danger { background: var(--danger-500); color: var(--surface); }
.btn--danger:hover:not(:disabled) { background: var(--danger-700); color: var(--surface); }

.btn--ghost { background: transparent; color: var(--ink-700); padding-inline: var(--space-2); }
.btn--ghost:hover:not(:disabled) { background: var(--ink-100); }

.btn--lg { padding: .75rem var(--space-5); font-size: 1rem; }
.btn--block { width: 100%; }

/* ---------- Forms --------------------------------------------------------- */

.field { margin-bottom: var(--space-4); }
.field > label, .field__label { display: block; font-weight: 600; font-size: .9375rem; margin-bottom: var(--space-1); }
.field__hint { display: block; font-size: .8125rem; color: var(--text-muted); margin-bottom: var(--space-2); }
.field__required { color: var(--danger-500); margin-inline-start: 2px; }

/* Purchase flow: brief essentials stay visible; supporting copy opens in place. */
.flow-note { font-size: .875rem; color: var(--text-muted); margin: 0 0 var(--space-4); }
.flow-disclosure {
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    background: var(--surface); margin-block: var(--space-4);
}
.flow-disclosure > summary {
    padding: .75rem var(--space-4); cursor: pointer; font-size: .875rem;
    font-weight: 600; min-height: 44px; overflow-wrap: anywhere;
}
.flow-disclosure > summary:hover { color: var(--brand-600); }
.flow-disclosure > summary:focus-visible {
    outline: 2px solid var(--brand-500); outline-offset: 3px; border-radius: var(--radius-sm);
}
.flow-disclosure[open] > summary { border-bottom: 1px solid var(--border); }
.flow-disclosure__body { padding: var(--space-4); }
.flow-disclosure__body > :last-child { margin-bottom: 0; }
.flow-disclosure--optional { background: var(--page-bg); box-shadow: none; }
.participant-card .flow-disclosure:last-child { margin-bottom: 0; }
.participant-consent { border-top: 1px solid var(--border); padding-top: var(--space-4); }
.participant-consent > label.checkbox, .participant-publication > label.checkbox, .flow-disclosure__body label.checkbox {
    display: flex; font-weight: 400; font-size: .875rem;
}
.participant-publication { border-top: 1px solid var(--border); padding-top: var(--space-4); }
.participant-publication > h3 { margin: 0 0 var(--space-3); font-size: .875rem; }
.participant-publication > .field__hint { margin: var(--space-2) 0 0; }
.representative-reuse { margin-top: var(--space-1); white-space: normal; text-align: start; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--space-4); }
.form-grid > * { min-width: 0; }
@media (max-width: 540px) {
    .form-grid { grid-template-columns: minmax(0, 1fr); }
}

/* Matches on what an input is NOT, rather than listing the types it might be.
   Blazor's <InputText> emits a bare <input> with no type attribute at all unless one is passed
   explicitly, so an `input[type=text]` selector silently skipped every plain text field - names,
   school, address - leaving them as unstyled browser rectangles while the email and number fields
   next to them looked right. Excluding the controls that must not look like text boxes is the only
   form of this rule that cannot be defeated by a missing attribute. */
input:not([type=checkbox], [type=radio], [type=submit], [type=button], [type=file], [type=range], [type=color], [type=hidden]),
select,
textarea {
    width: 100%; padding: .5625rem var(--space-3); font: inherit; font-size: .9375rem; color: var(--text);
    background: var(--surface); border: 1px solid var(--ink-400); border-radius: var(--radius-sm);
    transition: border-color .12s ease, box-shadow .12s ease;
}

input:not([type=checkbox], [type=radio]):hover, select:hover, textarea:hover { border-color: var(--ink-500); }
input:focus-visible, select:focus-visible, textarea:focus-visible {
    outline: none; border-color: var(--brand-500);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 25%, transparent);
}
/*  Checkboxes and radios keep a real outline.

    The rule above turns the outline off for every input and replaces it with a box-shadow ring, which
    is right for a text box - it follows the border - and wrong for these two: a native checkbox paints
    its own box inside the element, so a box-shadow lands behind it and is not visible at all. The
    result was a control with no focus indicator whatever, on the delivery choice, the gift, and the
    marketing agreement - three decisions about money and consent that a keyboard user could land on
    without being able to tell they had.

    2.4.7, and not a theoretical one: the gift checkbox changes what the card is charged. */
input[type=checkbox]:focus-visible,
input[type=radio]:focus-visible {
    outline: 2px solid var(--brand-500);
    outline-offset: 2px;
    box-shadow: none;
}

/*  --text-muted, not --ink-400. Placeholder text is text: 1.4.3 asks 4.5:1 of it, and --ink-400 gives
    4.09:1 on white and 4.34:1 on the dark ground - close enough to look deliberate and still short in
    both themes. --text-muted is the vetted muted tone, 7.0:1 light and 7.34:1 dark. */
input::placeholder { color: var(--text-muted); }

/* Inputs are full width by default, so text is never clipped. The narrower tiers below exist only
   where a field's width usefully signals how much is expected - a postcode in a full-width box invites
   a sentence - and every one is sized for its content in Armenian rather than for Latin digits.
   Erring wide: a field that is slightly too long costs nothing, one that truncates costs the user. */
.input--narrow { max-width: 9rem; }    /* pure digits: quantity, postcode, stock */
.input--code   { max-width: 15rem; }   /* short text or codes: unit, ADG code */
.input--money  { max-width: 15rem; }   /* an amount in drams, up to eight digits with separators */

/* A select's width is the width of its longest option, so it cannot be capped by a fixed max-width -
   that was clipping "— Ընտրեք —" on the grade picker. Sized to content instead, with a floor so a
   one-digit option list is still a comfortable target, and a ceiling of the container so a long option
   cannot push the form sideways. */
select {
    width: auto;
    min-width: 12rem;
    max-width: 100%;
}

.checkbox { display: flex; gap: var(--space-3); align-items: flex-start; font-weight: 500; cursor: pointer; }
.checkbox input[type=checkbox] { width: 1.125rem; height: 1.125rem; margin: .1875rem 0 0; flex: none; accent-color: var(--brand-500); cursor: pointer; }

/* Blazor validation hooks */
.valid.modified:not([type=checkbox]) { border-color: var(--success-700); }
.invalid { border-color: var(--danger-500); }
.validation-message { display: block; color: var(--danger-700); font-size: .8125rem; margin-top: var(--space-1); }
.validation-errors { margin: 0 0 var(--space-4); padding: var(--space-3) var(--space-4); background: var(--danger-50); border: 1px solid var(--danger-200); border-radius: var(--radius-sm); color: var(--danger-700); font-size: .875rem; }
.validation-errors ul { margin: 0; padding-inline-start: var(--space-4); }

/* ---------- Alerts & callouts --------------------------------------------- */

.alert { display: flex; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-radius: var(--radius-sm); border: 1px solid; font-size: .9375rem; }
.alert__icon { flex: none; font-size: 1rem; line-height: 1.5; }
.alert__body { min-width: 0; }
.alert--info { background: var(--brand-50); border-color: var(--brand-200); color: var(--brand-700); }
.alert--success { background: var(--success-50); border-color: var(--success-200); color: var(--success-700); }
.alert--warning { background: var(--warn-50); border-color: var(--warn-200); color: var(--warn-700); }
.alert--danger { background: var(--danger-50); border-color: var(--danger-200); color: var(--danger-700); }

/* A destructive, irreversible action. Visually distinct from a mere warning so
   "this cannot be undone" is legible before the click, not after. */
.danger-zone { border: 1px solid var(--danger-200); border-radius: var(--radius); background: var(--danger-50); padding: var(--space-5); }
.danger-zone__title { display: flex; align-items: center; gap: var(--space-2); margin: 0 0 var(--space-2); font-size: 1rem; font-weight: 700; color: var(--danger-700); }
.danger-zone__body { font-size: .9375rem; color: var(--ink-700); }
.danger-zone__consequences { margin: 0 0 var(--space-4); padding-inline-start: var(--space-5); font-size: .9375rem; color: var(--ink-700); }
.danger-zone__consequences li { margin-bottom: var(--space-1); }
.danger-zone__confirm { padding: var(--space-3); background: var(--surface); border: 1px solid var(--danger-200); border-radius: var(--radius-sm); margin-bottom: var(--space-4); }

/* ---------- Badges -------------------------------------------------------- */

.badge { display: inline-flex; align-items: center; gap: var(--space-1); padding: .125rem var(--space-2); border-radius: 999px; font-size: .75rem; font-weight: 650; border: 1px solid; white-space: nowrap; }
.badge--neutral { background: var(--ink-100); border-color: var(--border); color: var(--ink-700); }
.badge--info { background: var(--brand-50); border-color: var(--brand-200); color: var(--brand-700); }
.badge--success { background: var(--success-50); border-color: var(--success-200); color: var(--success-700); }
.badge--warning { background: var(--warn-50); border-color: var(--warn-200); color: var(--warn-700); }
/*  The basket table on a phone.

    Every column had white-space: nowrap - the headers, the numeric cells, and the "one participant per
    line" badge, which in Armenian is «Մեկ մասնակցի համար» and is by some way the widest single thing in
    the table. Together they held the table above every phone width in all three languages, so the
    checkout's first screen scrolled sideways for everyone on a mobile. It is inside .table-wrap and
    therefore scrolls rather than pushing the page, which is why nothing looked broken - it just meant
    the total was off-screen on the page whose job is showing the total.

    So the two rules that can wrap, wrap. The numeric cells keep nowrap: a price broken across two lines
    is worse than a narrow column, and they are short. */
@media (max-width: 40rem) {
    table.data thead th { white-space: normal; }
    table.data .badge { white-space: normal; text-align: center; border-radius: var(--radius-sm); }
}

.badge--danger { background: var(--danger-50); border-color: var(--danger-200); color: var(--danger-700); }

/* ---------- Tables -------------------------------------------------------- */

.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
table.data { width: 100%; border-collapse: collapse; font-size: .9375rem; }
table.data th, table.data td { padding: var(--space-3) var(--space-4); text-align: start; border-bottom: 1px solid var(--border); vertical-align: top; }
table.data thead th { background: var(--ink-50); font-size: .8125rem; font-weight: 650; text-transform: uppercase; letter-spacing: .03em; color: var(--text-muted); white-space: nowrap; }
table.data tbody tr:last-child td { border-bottom: none; }
table.data tbody tr:hover { background: var(--ink-50); }
.num { text-align: end; font-variant-numeric: tabular-nums; white-space: nowrap; }

/* Keep each basket item's controls together when a four-column table no longer fits. Explicit
   table roles in the markup preserve relationships when these cells switch to grid layout. */
.basket-items__label { display: none; }
@media (max-width: 40rem) {
    .basket-items { overflow: visible; }
    .basket-items table.data, .basket-items tbody { display: block; }
    .basket-items thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
    .basket-items tbody tr { display: grid; grid-template-columns: minmax(0, 1fr) auto; padding: var(--space-4); gap: var(--space-3); border-bottom: 1px solid var(--border); }
    .basket-items tbody tr:last-child { border-bottom: none; }
    .basket-items table.data td { display: block; border: 0; padding: 0; min-width: 0; white-space: normal; }
    .basket-items__name { grid-column: 1 / -1; overflow-wrap: anywhere; }
    .basket-items table.data td.basket-items__quantity { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
    .basket-items table.data td.basket-items__amount { display: flex; flex-direction: column; align-items: end; }
    .basket-items__action { grid-column: 1 / -1; justify-self: end; }
    .basket-items__label { display: inline; font-size: .8125rem; color: var(--text-muted); }
}

/* The static shell remains visible until the deferred route actually has content. No database or
   session-storage work is moved into prerendering just to show a heading and a retry link. */
.page:has(> .route-content > *) > .startup-shell { display: none; }
.startup-shell { padding-block: var(--space-5); }

/* Key/value detail list, used for order metadata */
.kv { display: grid; grid-template-columns: minmax(8rem, max-content) 1fr; gap: var(--space-2) var(--space-4); margin: 0; font-size: .9375rem; }
.kv dt { color: var(--text-muted); }
.kv dd { margin: 0; }

/* ---------- Product cards ------------------------------------------------- */

/* Registration and checkout share a form-first layout. Reference information follows the action. */
.purchase-page { max-width: 1060px; margin-inline: auto; }
.purchase-page > .steps, .purchase-page > nav { margin-bottom: 2rem; }
.purchase-heading { margin-bottom: 2rem; }
.purchase-kicker { color: var(--brand-500); font-size: .8125rem; font-weight: 700; letter-spacing: .06em; margin: 0 0 .625rem; }
.purchase-heading .page-header { margin-bottom: 0; }
.purchase-heading h1 { font-size: clamp(1.875rem, 3.2vw, 2.625rem); line-height: 1.15; margin-bottom: .75rem; }
.purchase-heading p:not(.purchase-kicker) { max-width: 42rem; }
.purchase-layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 2rem; align-items: start; }
.purchase-main { display: flex; flex-direction: column; gap: 1.5rem; min-width: 0; }
.purchase-main > .card + .card { margin-top: 0; }
.purchase-rail { position: sticky; top: 6rem; min-width: 0; }
.purchase-section, .purchase-payment {
    padding: 1.75rem; border: 1px solid var(--border); border-radius: 18px;
    background: var(--surface); box-shadow: none; min-width: 0;
}
.purchase-section__head { display: flex; justify-content: space-between; align-items: center; gap: 1rem; margin-bottom: 1.5rem; }
.purchase-section__head h2, .purchase-section__head .card__title { font-size: 1.125rem; margin: 0; }
.purchase-section__head .btn { margin-block: -.5rem; }
.count-selector { display: flex; flex-direction: column; gap: 1rem; align-items: center; justify-content: center; min-height: 150px; }
.count-selector .stepper { margin: 0; }
.steps__link, .steps__item { display: inline-flex; align-items: center; gap: var(--space-2); }
.steps__link { color: inherit; text-decoration: none; border-radius: var(--radius-sm); }
.steps__link:hover > span:not(.steps__num) { text-decoration: underline; text-underline-offset: .2em; }
.steps__link:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 4px; }
.purchase-page .field > label:not(.checkbox) { font-size: .875rem; margin-bottom: .5rem; }
.purchase-page input:not([type=checkbox], [type=radio], [type=submit], [type=button], [type=hidden]), .purchase-page select {
    min-height: 46px; border-radius: 9px; font-size: 1rem;
}
.purchase-page select { max-width: none; width: 100%; }
.purchase-page .form-grid { margin-bottom: 1.25rem; }
.purchase-section > .form-grid:last-child { margin-bottom: 0; }
.purchase-page .field { margin-bottom: 1.25rem; }
.purchase-page .field:last-child { margin-bottom: 0; }
.purchase-page .field__hint { line-height: 1.5; }
.purchase-page .checkbox { font-weight: 400; line-height: 1.55; font-size: .875rem; }
.purchase-page .field > label.checkbox { display: flex; font-weight: 400; }
.consent-panel { margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--border); }
.consent-panel .participant-consent { padding-top: 0; border-top: 0; }
.consent-panel .participant-publication { padding-top: 1.25rem; }
.participant-add {
    display: flex; align-items: center; justify-content: center; gap: .75rem; min-height: 60px; padding: 1rem;
    border: 1px dashed var(--border-control); border-radius: 14px; background: transparent;
    color: var(--brand-700); font: inherit; font-size: .9375rem; font-weight: 600; cursor: pointer;
}
.participant-add > span { font-size: 1.5rem; font-weight: 400; }
.participant-remove { flex-shrink: 0; }
.participant-add:hover:not(:disabled) { background: var(--brand-50); border-color: var(--brand-500); }
.participant-add:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 3px; }
.participant-add:disabled { opacity: .5; cursor: not-allowed; }
.summary.order-overview { padding: 1.5rem; border-radius: 18px; background: var(--surface); }
.order-overview__heading { display: flex; gap: 1rem; justify-content: space-between; align-items: baseline; margin-bottom: 1.25rem; }
.order-overview__heading h2 { margin: 0; font-size: 1.125rem; }
.order-overview__heading a { font-size: .8125rem; }
.order-overview .summary__line { align-items: flex-start; gap: .75rem; padding-block: .875rem; font-size: .875rem; }
.order-overview .summary__line > :first-child { min-width: 0; overflow-wrap: anywhere; }
.order-overview .summary__line .num { flex-shrink: 0; }
.order-overview .summary__sub { font-size: .75rem; margin-top: .375rem; line-height: 1.45; }
.order-overview .summary__total { gap: 1rem; margin-top: .5rem; padding-top: 1.25rem; align-items: baseline; }
.order-overview .summary__total > :first-child { font-size: .875rem; font-weight: 500; }
.order-overview .summary__total > :last-child { font-size: 1.375rem; white-space: nowrap; }
.order-overview__footer { margin-top: 1.5rem; }
.purchase-actions { display: flex; gap: 1rem; align-items: center; }
.purchase-action-buttons { display: flex; flex: 1; gap: .625rem; align-items: center; min-width: 0; }
.purchase-action-buttons .btn--primary { flex: 1; min-width: 0; }
.purchase-back { flex: 0 0 auto; padding-inline: .75rem; white-space: nowrap; }
.purchase-actions .btn--primary { justify-content: center; gap: .75rem; white-space: normal; }
.purchase-action-total { display: none; }
.purchase-support { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.5rem 2.5rem; margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--border); }
.purchase-support__card { min-width: 0; padding: 1.25rem; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); font-size: .8125rem; line-height: 1.6; color: var(--text-muted); }
.purchase-support__card h2 { font-size: .9375rem; color: var(--text); margin: 0 0 .625rem; }
.purchase-support__card p { margin: 0 0 .75rem; }
.purchase-support .flow-disclosure { background: transparent; border: 0; margin: .5rem 0 0; }
.purchase-support .flow-disclosure > summary { padding: .375rem 0; min-height: 36px; }
.purchase-support .flow-disclosure__body { padding: .75rem 0 0; }
#participant-data-summary { scroll-margin-top: 6rem; }
.contact-identity { display: flex; justify-content: space-between; align-items: center; gap: 1rem; margin-bottom: 1.5rem; padding: 1rem; border: 1px solid var(--border); border-radius: 10px; background: var(--page-bg); }
.contact-identity__name { display: block; font-weight: 600; overflow-wrap: anywhere; }
.purchase-page .delivery-choice { padding: 1rem; margin: 0; border: 1px solid var(--border-control); border-radius: 10px; }
.purchase-page .delivery-choice:has(input:checked) { border-color: var(--brand-500); background: var(--brand-50); }
.purchase-page .delivery-choice + .field { margin-top: 1.25rem; }
.purchase-payment { max-width: calc(100% - 352px); margin-top: 1.5rem; }
.purchase-payment > :first-child { margin-top: 0; }
.purchase-payment > .purchase-actions { margin-top: 1.5rem; justify-content: space-between; }
.purchase-payment .flow-disclosure { margin-top: 0; }
.purchase-page .secured { padding: .875rem 1rem; margin-bottom: 1.5rem; font-size: .875rem; }
.purchase-page .offers { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.purchase-page .offer__image { aspect-ratio: 4 / 3; object-fit: contain; padding: 1rem; }
.purchase-page .offer__image--empty { min-height: 150px; }
.purchase-page .offer__body { padding: 1.25rem; }
.purchase-page .offer__foot { padding: 0 1.25rem 1.25rem; }
.purchase-page .offer__desc { font-size: .8125rem; line-height: 1.6; }
.purchase-page .offer__price { font-size: 1.25rem; }
.purchase-page .flow-nav { margin-top: 0; padding-top: 0; border: 0; }
@media (max-width: 900px) {
    .purchase-layout { grid-template-columns: minmax(0, 1fr) 280px; gap: 1.25rem; }
    .purchase-section, .purchase-payment, .order-overview { padding: 1.25rem; }
    .purchase-payment { max-width: calc(100% - 300px); }
    .purchase-page .offers { grid-template-columns: 1fr; }
}
@media (max-width: 700px) {
    .purchase-page { padding-bottom: 6rem; }
    .purchase-layout { grid-template-columns: minmax(0, 1fr); gap: 1.5rem; }
    .purchase-rail { position: static; }
    .purchase-heading { margin-bottom: 1.5rem; }
    .purchase-heading h1 { font-size: 1.875rem; }
    .purchase-support { grid-template-columns: 1fr; gap: 1.25rem; margin-top: 2rem; padding-top: 1.25rem; }
    .purchase-payment { max-width: none; }
    .purchase-actions { position: fixed; bottom: 0; inset-inline: 0; z-index: 30; background: var(--surface); border-top: 1px solid var(--border); padding: .875rem 1rem max(.875rem, env(safe-area-inset-bottom)); box-shadow: 0 -4px 16px #0000000d; }
    .purchase-action-total { display: flex; flex-direction: column; flex: 0 0 auto; gap: .125rem; font-size: .75rem; }
    .purchase-action-total strong { font-size: 1rem; white-space: nowrap; }
    .purchase-actions .btn--primary { flex: 1; min-width: 0; font-size: .9375rem; padding-inline: .875rem; }
    .purchase-actions .btn--secondary { flex: 0 1 auto; font-size: .8125rem; padding-inline: .625rem; }
    .purchase-actions .btn--lg { min-height: 48px; }
    .purchase-page .steps { display: grid; grid-template-columns: repeat(4, auto); font-size: .75rem; gap: .375rem; }
    .purchase-page .steps li { flex-direction: column; gap: .375rem; text-align: center; min-width: 0; }
    .purchase-page .steps__link, .purchase-page .steps__item { flex-direction: column; gap: .375rem; max-width: 100%; }
    .purchase-page .steps__link > span:not(.steps__num), .purchase-page .steps__item > span:not(.steps__num) { max-width: 100%; white-space: nowrap; }
    .purchase-page .steps li::after { display: none; }
}
@media (max-width: 380px) {
    .purchase-page { padding-bottom: 7.5rem; }
    .purchase-actions:has(.purchase-action-total) { flex-wrap: wrap; gap: .5rem; }
    .purchase-action-total { flex-basis: 100%; flex-direction: row; justify-content: space-between; align-items: baseline; }
    .purchase-action-buttons { flex-basis: 100%; }
}

.product { display: flex; flex-direction: column; gap: var(--space-2); }
.product__name { font-size: 1.0625rem; margin: 0; }
.product__desc { color: var(--text-muted); font-size: .9375rem; margin: 0; }
.product__price { font-size: 1.25rem; font-weight: 700; }

/*  The call to action, pinned to the bottom of the card.

    margin-top: auto is the whole mechanism: the card is a flex column stretched to its grid row, the
    auto margin eats every pixel of leftover space above the button, and so a row of cards puts its
    buttons on one baseline however differently the titles, descriptions, prices and stock badges above
    them happen to wrap.

    It replaces `flex: 1 0 auto` on .product__desc, which used to take up the slack instead. That worked
    only for as long as every product had a description - and this page renders the description
    conditionally, because the catalogue lets one be blank. Measured at 1440px with the middle and right
    products' descriptions removed: their buttons floated 94.94px above the card's bottom edge while the
    left one's sat 25px above it, putting the row 69.93px out of line. Bottom-aligning the FOOT rather
    than growing a sibling that may not exist cannot come apart that way; with the same content the row
    now measures 0px.

    padding-top rather than a margin, because margin-top is spent on the alignment: it keeps the button
    the same 16px clear of the text above it (8px of it the column's own gap) that the old
    margin-top: var(--space-2) gave. */
.product__foot { margin-top: auto; padding-top: var(--space-2); }

/* ---------- Home page routing choices ------------------------------------- */

.choices { display: grid; gap: var(--space-3); }
.choice {
    display: flex; flex-direction: column; gap: var(--space-1);
    padding: var(--space-5); background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); text-decoration: none; color: inherit;
    box-shadow: var(--shadow-sm); transition: border-color .12s ease, box-shadow .12s ease;
}
.choice:hover { border-color: var(--brand-300); box-shadow: var(--shadow); color: inherit; }
.choice:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
.choice__title { font-size: 1.0625rem; }
.choice__desc { color: var(--text-muted); font-size: .9375rem; }

/* ---------- Upsell: the one persuasive page ------------------------------- */

/* Reassurance first. A parent will not weigh a second purchase while still unsure the first worked. */
.secured {
    display: flex; gap: var(--space-3); align-items: center;
    padding: var(--space-4); margin-bottom: var(--space-5);
    background: var(--success-50); border: 1px solid var(--success-200);
    border-radius: var(--radius); color: var(--success-700);
}
.secured__tick {
    display: grid; place-items: center; flex: none;
    width: 1.75rem; height: 1.75rem; border-radius: 999px;
    background: var(--success-700); color: var(--surface); font-weight: 700;
}

.media { border-radius: var(--radius); overflow: hidden; background: var(--ink-100); }
.media video { display: block; width: 100%; height: auto; }

.offers { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }

.offer {
    position: relative; display: flex; flex-direction: column;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
    overflow: hidden; box-shadow: var(--shadow-sm);
    transition: border-color .12s ease, box-shadow .12s ease, transform .12s ease;
}
.offer:hover { box-shadow: var(--shadow); transform: translateY(-2px); }

/* The better-value option is meant to win the glance - a heavier border and a flag, not a trick. */
.offer--best { border-color: var(--brand-300); border-width: 2px; box-shadow: var(--shadow); }
.offer--chosen { border-color: var(--success-700); border-width: 2px; }

.offer__flag {
    position: absolute; inset-block-start: 0; inset-inline-end: 0;
    padding: var(--space-1) var(--space-3);
    background: var(--accent-500); color: var(--accent-700);
    font-size: .8125rem; font-weight: 700;
    border-end-start-radius: var(--radius);
}

/*  Portrait, for the same reason .product__image is - the real covers are portrait, and the landscape
    box this used to be cropped straight through the title and the year. cover rather than contain is
    safe now that the box and the source are within a few percent of each other: it fills edge to edge
    and takes a sliver off instead of a band out of the middle.

    NOT the same ratio as .product__image, which is now 4/5 because that is what the covers in covers/
    measure (800x1000). This box is still 3/4, which against a 0.80 source crops about 3% off each
    vertical edge. Left alone deliberately: it belongs to the Bebras books step, which needs a headcount
    before it will render an offer and so was not measured while the shop cards were - and an unmeasured
    layout change is how the shop got here. Worth revisiting with that page in front of you. */
.offer__image { display: block; width: 100%; aspect-ratio: 3 / 4; object-fit: cover; background: var(--ink-100); }
.offer__image--empty {
    display: grid; place-items: center; padding: var(--space-4);
    color: var(--text-muted); font-size: .8125rem; text-align: center;
    background: linear-gradient(135deg, var(--ink-100), var(--brand-50));
}

.offer__body { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); flex: 1 0 auto; }
.offer__name { margin: 0; font-size: 1.0625rem; }
.offer__desc { margin: 0; font-size: .9375rem; color: var(--text-muted); flex: 1 0 auto; }

.offer__pricing { display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
.offer__price { font-size: 1.5rem; font-weight: 700; }
.offer__was { color: var(--text-muted); text-decoration: line-through; font-size: 1rem; }

/*  The shop card's equivalent of .offer__was. Same treatment, smaller, because the shop card's price
    sits on one line with it rather than in a dedicated pricing row. */
.product__was { color: var(--text-muted); text-decoration: line-through; font-size: 0.875rem; margin-left: 0.375rem; }

.offer__scarce { font-size: .875rem; font-weight: 600; color: var(--danger-700); }

.offer__foot { padding: 0 var(--space-4) var(--space-4); }

@media (max-width: 30rem) {
    .offers { grid-template-columns: 1fr; }
}

/* ---------- Steps / progress ---------------------------------------------- */

.steps { display: flex; flex-wrap: wrap; gap: var(--space-2); list-style: none; margin: 0 0 var(--space-5); padding: 0; font-size: .875rem; }
.steps li { display: flex; align-items: center; gap: var(--space-2); color: var(--text-muted); }
.steps li::after { content: ""; width: 1.25rem; height: 1px; background: var(--border); margin-inline-start: var(--space-1); }
.steps li:last-child::after { display: none; }
.steps__num { display: grid; place-items: center; width: 1.5rem; height: 1.5rem; border-radius: 999px; background: var(--ink-100); color: var(--text-muted); font-size: .75rem; font-weight: 700; flex: none; }
.steps li.is-current { color: var(--text); font-weight: 600; }
.steps li.is-current .steps__num { background: var(--brand-500); color: var(--surface); }
.steps li.is-done .steps__num { background: var(--success-700); color: var(--surface); }  /* 6.1:1 light, 8.9:1 dark */

/* ---------- Flow controls ------------------------------------------------- */

/* Headcount picker. Big targets because this is the first thing anyone touches, often on a phone. */
.stepper { display: flex; align-items: center; gap: var(--space-4); }
.stepper__btn {
    width: 2.75rem; height: 2.75rem; flex: none;
    display: grid; place-items: center;
    font-size: 1.5rem; line-height: 1; font-family: inherit;
    background: var(--surface); color: var(--brand-700);
    /* An edge you can see, for the same reason as .btn--secondary: this is a control, not a divider,
       and it is the first thing anyone touches in the registration flow. */
    border: 1px solid var(--border-control); border-radius: var(--radius-sm);
    cursor: pointer;
}
.stepper__btn:hover:not(:disabled) { background: var(--brand-50); border-color: var(--brand-300); }
.stepper__btn:disabled { opacity: .4; cursor: not-allowed; }
.stepper__value { font-size: 1.75rem; font-weight: 700; min-width: 2rem; text-align: center; font-variant-numeric: tabular-nums; }

/* Back / running total / forward, in that order, on every step. */
.flow-nav {
    display: flex; align-items: center; gap: var(--space-3);
    margin-top: var(--space-5); padding-top: var(--space-4);
    border-top: 1px solid var(--border);
}
.flow-nav__total { margin-inline: auto; display: flex; flex-direction: column; align-items: center; line-height: 1.2; }

@media (max-width: 30rem) {
    .flow-nav { flex-wrap: wrap; }
    .flow-nav__total { order: -1; width: 100%; align-items: flex-start; margin-inline: 0; margin-bottom: var(--space-2); }
    .flow-nav .btn--lg { flex: 1 0 auto; }
}

/* Compact quantity control inside an offer card - reuses the stepper buttons. */
.qty { display: flex; align-items: center; gap: var(--space-2); }
.qty__value { font-size: 1.125rem; font-weight: 700; min-width: 1.75rem; text-align: center; font-variant-numeric: tabular-nums; }
.qty__total { margin-inline-start: auto; font-weight: 650; }

/* Type-to-search picker (schools). Absolute list so it overlays what follows rather
   than shoving the form down as suggestions appear. */
.picker { position: relative; }
.picker__input { width: 100%; }
.picker__list {
    position: absolute; inset-inline: 0; top: calc(100% + 2px); z-index: 20;
    margin: 0; padding: 0; list-style: none;
    max-height: 15rem; overflow-y: auto;
    background: var(--surface); border: 1px solid var(--brand-300);
    border-radius: var(--radius-sm); box-shadow: var(--shadow);
}
.picker__list--empty { padding: var(--space-3) var(--space-4); font-size: .875rem; color: var(--text-muted); }
.picker__option {
    display: flex; justify-content: space-between; gap: var(--space-3);
    width: 100%; padding: var(--space-2) var(--space-3);
    font: inherit; font-size: .9375rem; text-align: start;
    background: none; border: 0; border-bottom: 1px solid var(--border);
    cursor: pointer; color: inherit;
}
.picker__option:last-child { border-bottom: 0; }
.picker__option:hover, .picker__option[aria-selected="true"] { background: var(--brand-50); }
.picker__meta { color: var(--text-muted); font-size: .8125rem; }

/* A school already chosen reads as settled rather than as an editable field, so a stray keystroke
   cannot silently clear it. */
.picker__chosen {
    display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
    padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);
    background: var(--brand-50); border: 1px solid var(--brand-300);
    border-radius: var(--radius-sm);
}

/* ---------- Basket summary ------------------------------------------------ */

.summary { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); overflow: hidden; }
.summary__line { display: flex; justify-content: space-between; gap: var(--space-4); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); font-size: .9375rem; }
/*  A line whose caveat sits on the row below it: the border moves to the note, so the two read as one
    entry rather than two. */
/*  The delivery-or-collection choice, which follows the contact fields directly.

    The space above is deliberately larger than the gap between two form fields: this is not another
    field to fill in, it is a decision that changes what the card is charged and whether an address is
    asked for at all. Sitting one field-gap below a phone input, it read as the last item of the form
    rather than a question about the order. */
.delivery-choice { margin: var(--space-6) 0 var(--space-4); }

.summary__line--tight { border-bottom: 0; padding-bottom: 0; }

/*  A summary row that is a real description list.

    <dl> carries the label/value pairing in dt and dd, and a screen reader announces the pair as one
    entry; the admin bank-operations panel used div > span > span, so "Response code" and "00" arrived
    as two unrelated strings. dd's browser default indent has to go, or the value drifts 40px right of
    where the flex layout puts it. */
.summary__line > dt,
.summary__line > dd { margin: 0; }
.summary__line > dt { font-weight: inherit; }

/*  A detail belonging to the line above it - which participant, which option - on its own row so it
    cannot run into the product name or be stranded mid-wrap by a separator. */
.summary__sub { display: block; margin-top: .125rem; font-size: .8125rem; color: var(--text-muted); }

/*  The aside under a summary line - how a fee behaves, not what it costs. Smaller and italic so it is
    plainly secondary to the figure above, and indented to the same edge as the label it belongs to. */
.summary__note {
    padding: .1875rem var(--space-4) var(--space-3);
    border-bottom: 1px solid var(--border);
    font-size: .8125rem;
    font-style: italic;
    color: var(--text-muted);
}

.summary__total { display: flex; justify-content: space-between; gap: var(--space-4); padding: var(--space-4); font-weight: 700; font-size: 1.0625rem; background: var(--ink-50); }

/* ---------- Icons ------------------------------------------------------------ */

/*  Sized in em so an icon matches whatever text it sits beside, and coloured by currentColor so it
    inherits the button it is in rather than needing a rule per variant. */
.icon { width: 1em; height: 1em; flex: none; vertical-align: -0.125em; }
.icon--lg { width: 1.25em; height: 1.25em; }

/*  An icon-only control still has to be a real target. 44px is the smallest comfortable one, and a row
    of them in a table is exactly where a 20px hit area hurts - see .btn--icon below. */
.btn--icon {
    display: inline-grid; place-items: center;
    width: 2.25rem; height: 2.25rem; padding: 0;
    background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm);
    color: var(--ink-700); cursor: pointer;
}
.btn--icon:hover:not(:disabled) { background: var(--ink-100); border-color: var(--border-control); }
.btn--icon:disabled { opacity: .4; cursor: not-allowed; }
.btn--icon--danger { color: var(--danger-700); }
.btn--icon--danger:hover:not(:disabled) { background: var(--danger-50); border-color: var(--danger-200); }
.btn--icon { min-width: 2.75rem; min-height: 2.75rem; font-size: 1.125rem; text-decoration: none; }
.btn--icon:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
.btn--icon--primary { background: var(--brand-500); color: var(--surface); border-color: var(--brand-500); }
.btn--icon--primary:hover:not(:disabled) { background: var(--brand-700); color: var(--surface); border-color: var(--brand-700); }

/*  A row of them, kept from wrapping into a ragged column on a narrow table cell. */
.row-actions { display: inline-flex; gap: var(--space-1); align-items: center; }

/* ---------- Page help ------------------------------------------------------- */

/*  The question mark beside a page title. Quiet until wanted - it is an offer, not an instruction -
    but a real target, and never smaller than the heading it sits beside. */
/*  The title and its help trigger on one line, wrapping together rather than the mark stranding
    itself on a line of its own when the heading is long - which in Armenian it usually is.

    A wrapper around the h1 rather than the h1 itself, so the heading's own text content stays exactly
    the title - see the comment in PageHeader.razor. */
.page-header__title-row { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-2); }
.page-header__title-row > h1 {
    margin: 0;
    /* Long translated words must fit a narrow screen without widening the title's flex row. */
    min-width: 0;
    overflow-wrap: anywhere;
}

.page-help__trigger {
    display: inline-grid; place-items: center;
    width: 1.75rem; height: 1.75rem; margin-inline-start: var(--space-2);
    padding: 0; border: 1px solid var(--border-control); border-radius: 999px;
    background: var(--surface); color: var(--text-muted);
    font-size: 1rem; cursor: pointer; vertical-align: middle;
}
.page-help__trigger:hover { color: var(--brand-700); border-color: var(--brand-300); }

/*  The panel itself. A native <dialog>, so ::backdrop is the browser's and the centring is too. */
.page-help {
    width: min(42rem, calc(100vw - 2rem));
    max-height: min(80vh, 44rem);
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow-lg);
}
.page-help::backdrop { background: rgb(0 0 0 / 45%); }

.page-help__head {
    display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4);
    padding: var(--space-5) var(--space-5) var(--space-3);
    border-bottom: 1px solid var(--border);
}
.page-help__title { margin: 0; font-size: 1.125rem; }
.page-help__close {
    flex: none; display: grid; place-items: center;
    width: 2rem; height: 2rem; padding: 0;
    background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm);
    color: var(--text-muted); cursor: pointer;
}
.page-help__close:hover { background: var(--ink-100); color: var(--text); }

/*  Scrolls on its own rather than letting the dialog grow past the viewport - the close button has to
    stay reachable however long the instructions get. */
.page-help__body { padding: var(--space-5); overflow-y: auto; font-size: .9375rem; line-height: 1.6; }
.page-help__body h3 { margin: var(--space-5) 0 var(--space-2); font-size: 1rem; }
.page-help__body h3:first-child { margin-top: 0; }
.page-help__body p { margin: 0 0 var(--space-3); }
.page-help__body ol, .page-help__body ul { margin: 0 0 var(--space-4); padding-inline-start: 1.25rem; }
.page-help__body li { margin-bottom: var(--space-2); }
.page-help__body li:last-child { margin-bottom: 0; }

/*  The one thing a reader most needs to not get wrong on each screen. */
.page-help__caution {
    margin: var(--space-4) 0 0;
    padding: var(--space-3) var(--space-4);
    background: var(--warn-50); border: 1px solid var(--warn-200); border-radius: var(--radius-sm);
    color: var(--warn-700);
}

/*  Looks like .btn--secondary without BEING it - see the comment in PageHelp.razor. A dialog is in the
    DOM whether it is open or not, so anything it shares a class with, every page's tests start finding. */
.page-help__dismiss {
    display: inline-flex; align-items: center; gap: var(--space-2);
    padding: .5rem var(--space-4); font: inherit; font-size: .9375rem; font-weight: 600;
    background: var(--surface); color: var(--ink-700);
    border: 1px solid var(--border-control); border-radius: var(--radius-sm);
    cursor: pointer;
}
.page-help__dismiss:hover { background: var(--ink-100); color: var(--text); }

.page-help__foot {
    display: flex; justify-content: flex-end;
    padding: var(--space-3) var(--space-5) var(--space-5);
    border-top: 1px solid var(--border);
}

@media (max-width: 40rem) {
    /*  Full width on a phone, and anchored to the bottom where a thumb is. */
    .page-help { width: 100vw; max-width: 100vw; max-height: 88vh; margin: auto auto 0; border-radius: var(--radius) var(--radius) 0 0; }
}

/* ---------- Empty / loading states ---------------------------------------- */

.empty { text-align: center; padding: var(--space-8) var(--space-4); color: var(--text-muted); border: 1px dashed var(--border); border-radius: var(--radius); background: var(--surface); }
.empty__title { color: var(--text); margin-bottom: var(--space-1); }
.loading { color: var(--text-muted); padding: var(--space-5) 0; }

/* ---------- Environment banner -------------------------------------------- */

.env-banner { padding: var(--space-2) var(--space-4); font-size: .875rem; font-weight: 600; text-align: center; }
.env-banner--sandbox { background: var(--warn-50); color: var(--warn-700); border-bottom: 1px solid var(--warn-200); }
.env-banner--production { background: var(--success-50); color: var(--success-700); border-bottom: 1px solid var(--success-200); }

/* ---------- Blazor framework UI ------------------------------------------- */

.blazor-error-boundary { background: var(--danger-700); padding: var(--space-4); color: var(--white); border-radius: var(--radius-sm); }
.blazor-error-boundary::after { content: "An error has occurred."; }

/* ---------- Responsive ---------------------------------------------------- */

@media (max-width: 40rem) {
    .page { padding: var(--space-4) var(--space-3) var(--space-6); }
    h1 { font-size: 1.5rem; }
    .site-header__inner { padding: var(--space-3); }
    .site-header__name span:last-child { display: none; }
    .kv { grid-template-columns: 1fr; gap: 0; }
    .kv dt { margin-top: var(--space-2); font-size: .8125rem; }
    .btn--responsive-block { width: 100%; }
}

/* ---------- Dark mode ------------------------------------------------------
   Follows the visitor's OS setting. Tokens only - no component rules repeated,
   so the two themes cannot drift apart. */

@media (prefers-color-scheme: dark) {
    :root {
        --page-bg: #12171a;
        --admin-shell-bg: #141a1d;
        --surface: #1a2125;
        --border: #2b3439;
        --text: #e6eae9;
        --text-muted: #a7b0b3;

        --ink-50: #1f272b;
        --ink-100: #232c31;
        --ink-200: #2b3439;

        /* 3.3:1 on --surface, so an outlined control still has an edge on the dark ground. The first
           value tried here was #5d686d, which looked right and measured 2.85:1 - which is the whole
           reason ColourContrastTests computes these rather than trusting the comment beside them. */
        --border-control: #687277;
        --ink-400: #79868b;

        /* Was missing, so the light value (#4f585e - a dark grey meant to sit on white) leaked into
           dark: the language switcher's label rendered at 2.24:1 on --surface, and input hover borders
           fell below the 3:1 that WCAG 1.4.11 asks of a control boundary. Same tone as --text-muted,
           which is already the vetted muted colour here: 7.34:1 on --surface. */
        --ink-500: #a7b0b3;

        --ink-700: #cfd6d6;

        /* The navy family collapses into the ground on dark, so the ramp inverts - 700 becomes the
           lightest - and risefoundation.am's cyan takes the interactive role. The logo navy #005271 as
           a fill here would be 2.10:1 against the page, which is a button you cannot find. --brand-300
           is the one rung that does NOT change: it is a border and a tint, and the teal reads on both
           grounds. These values are unchanged by the move to the logo navy - this block was already in
           the logo's hue family, at 189-193, and it is the LIGHT ramp that has just joined it.
             --brand-500 #4ec3d9 with --surface   7.87:1  (the comment here said 8.5:1 and was wrong)
             --brand-600 #7ad3e4 with --surface   9.52:1
             --brand-700 #a5e2ee on --surface    11.42:1 */
        --brand-50: #10262c;
        --brand-100: #16333b;
        --brand-200: #1f4a55;
        --brand-500: #4ec3d9;
        --brand-600: #7ad3e4;
        --brand-700: #a5e2ee;

        /*  Inverted like the brand ramp: on dark the fill is light and .btn--primary's --surface label
            is the dark one. The brick #9a2b1e as a fill would sit at 2.35:1 against the page, so it is
            the same colour lightened - hue 6.3 and its saturation kept, lightness taken up.

            It has to STAY in the giving red's family rather than reverting to the blue that used to be
            here (#7dc4e8): that blue was 10.7 degrees of hue from this block's own --brand-500 cyan, so
            in dark mode the donate button and the primary button were near enough the same colour, and
            the one reason this token exists - not reading as one more brand button - held in light only.

            The dark danger tones are also warm reds (--danger-500 #f4685f), and no lightness separates
            them from this by contrast: they are 1.19:1 apart. It is bounded rather than fixed, because
            they cannot meet - .btn--danger is admin-only and the donate button is in the storefront
            header, which AdminLayout does not render. Put a danger button on the storefront and this
            needs revisiting.
              --donate-500 #e68a7f with a --surface label  6.45:1
              --donate-600 #eda9a1 with a --surface label  8.38:1 */
        --donate-500: #e68a7f;
        --donate-600: #eda9a1;

        --accent-50:  #2a2010;
        --accent-200: #4a3c1c;
        /* Amber and its ink both hold. Lightening --accent-700 here is what made .offer__flag
           light-gold-on-gold at 1.2:1 - it is ink on a fill, and the fill does not change. */
        --accent-500: #f0a81e;
        --accent-700: #5a2c05;

        --success-50: #0f2a24;
        --success-200: #1a5145;
        --success-700: #5fd3b4;

        --warn-50: #2a2010;
        --warn-200: #55421a;
        --warn-700: #f0b866;

        --danger-50: #2a1416;
        --danger-200: #5d2429;
        --danger-500: #f4685f;
        --danger-700: #ff9c99;

        --shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);
        --shadow: 0 1px 3px rgba(0, 0, 0, .5), 0 4px 12px rgba(0, 0, 0, .35);
        --shadow-lg: 0 8px 30px rgba(0, 0, 0, .5);
    }

}

@media (prefers-reduced-motion: reduce) {
    * { transition: none !important; animation: none !important; }
}

/* ---- admin shell -------------------------------------------------------------------------------
   A persistent sidebar rather than a top bar: this is an application with a small fixed set of
   sections, and an operator working through orders should never go via a home page to reach receipts.
   Deliberately unlike the storefront - the panel refunds money and files permanent tax records. */

.admin-shell { display: flex; min-height: 100vh; align-items: stretch; }

.admin-sidebar {
  width: 15rem; flex: none; display: flex; flex-direction: column;
  background: var(--admin-shell-bg); color: #fff; position: sticky; top: 0; height: 100vh;
}
.admin-navigation { display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0; }
.admin-menu-toggle { display: none; }
.admin-menu-toggle__close, .admin-menu-toggle[aria-expanded=true] .admin-menu-toggle__open { display: none; }
.admin-menu-toggle[aria-expanded=true] .admin-menu-toggle__close { display: block; }

.admin-brand { display: block; padding: var(--space-4); color: #fff; text-decoration: none; border-bottom: 1px solid rgba(255,255,255,.12); }
.admin-brand__mark { display: block; font-weight: 800; letter-spacing: .04em; }
.admin-brand__sub { display: block; font-size: .75rem; opacity: .65; margin-top: .125rem; }

.admin-side-nav { flex: 1 1 auto; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #ffffff40 transparent; padding: var(--space-3) 0; }
.admin-side-nav__group { margin-bottom: var(--space-4); }
.admin-side-nav__label {
  font-size: .6875rem; text-transform: uppercase; letter-spacing: .08em;
  opacity: .5; padding: 0 var(--space-4) var(--space-1);
}
.admin-side-nav__link {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  padding: .5rem var(--space-4); color: #fff; text-decoration: none; font-size: .9375rem;
  border-inline-start: 3px solid transparent;
}
.admin-side-nav__link:hover { background: rgba(255,255,255,.07); }
.admin-side-nav__submenu { display: grid; gap: .15rem; margin: .25rem .6rem .65rem 1.2rem; padding-left: .6rem; border-left: 1px solid #ffffff25; }
.admin-side-nav__sublink { padding: .55rem .7rem; min-height: 2.5rem; font-size: .83rem; color: inherit; opacity: .8; border-radius: .4rem; text-decoration: none; }
.admin-side-nav__sublink:hover { background: #ffffff0b; opacity: 1; }
.admin-side-nav__sublink.active { background: #ffffff16; color: #fff; opacity: 1; }
.admin-side-nav__sublink:focus-visible { outline: 2px solid var(--brand-300); outline-offset: 2px; }
/* Blazor's NavLink adds .active to whichever section is open. */
.admin-side-nav__link.active { background: rgba(255,255,255,.12); border-inline-start-color: var(--brand-300); font-weight: 600; }

/*  The count of receipts the tax authority is still owed, so it has to be legible in both themes.
    --danger-500 lightens in dark mode to work as a FILL under dark text; white on it is 3.00:1 there,
    below the 4.5:1 this 12px number needs. --danger-700 is the ink tone of that family and holds on
    both grounds: white on it is 7.6:1 light, and in dark mode the pair inverts to dark-on-light. */
.admin-side-nav__badge {
  background: var(--danger-700); color: var(--surface); font-size: .75rem; font-weight: 700;
  min-width: 1.25rem; padding: 0 .375rem; border-radius: 999px; text-align: center;
}

.admin-sidebar__foot { flex: none; padding: var(--space-4); border-top: 1px solid rgba(255,255,255,.12); font-size: .8125rem; }
.admin-env { display: inline-block; padding: .125rem .5rem; border-radius: 999px; font-size: .6875rem; font-weight: 700; margin-bottom: var(--space-2); }
.admin-env--production { background: rgba(255,255,255,.9); color: #1c6b3a; }
.admin-env--sandbox { background: rgba(255,255,255,.9); color: #8a5a00; }
.admin-sidebar__user { opacity: .6; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: var(--space-2); }
.admin-sidebar__links { display: flex; gap: var(--space-3); }
.admin-sidebar__links a { color: #fff; opacity: .8; text-decoration: underline; }
.admin-sidebar__links a:hover { opacity: 1; }

.admin-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.admin-content { flex: 1 1 auto; min-width: 0; max-width: var(--content-width); width: 100%; margin: 0 auto; padding: var(--space-5) var(--space-4); }
.admin-content .table-wrap { position: relative; min-width: 0; max-width: 100%; }
.admin-foot { border-top: 1px solid var(--border); padding: var(--space-4); font-size: .8125rem; color: var(--text-muted); text-align: center; }

/*  Denser tables: an operator scanning orders wants more rows visible than a buyer does - and, it turns
    out, more COLUMNS. The inline padding is the quiet giant here: 1rem each side across eight columns is
    256px of a 1054px table spent on whitespace, which measured out as the last 13px the products table
    was over its container by. 0.75rem gives 128px of it back, which is a whole column. */
.admin-content .data th, .admin-content .data td { padding-block: .5rem; padding-inline: .75rem; }

@media (max-width: 60rem) {
  .admin-shell { display: block; }
  .admin-sidebar { width: 100%; height: auto; position: static; flex-direction: row; flex-wrap: wrap; align-items: center; gap: var(--space-2); padding: var(--space-3) var(--space-4); }
  .admin-brand { border-bottom: 0; padding: 0; margin-inline-end: auto; }
  .admin-sidebar[data-menu-ready] .admin-menu-toggle { display: inline-grid; flex: none; color: #fff; border: 1px solid #ffffff40; }
  .admin-menu-toggle:focus-visible { outline: 2px solid var(--brand-300); outline-offset: 3px; }
  .admin-navigation { flex-basis: 100%; max-height: 75vh; max-height: 75dvh; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #ffffff40 transparent; border-top: 1px solid #ffffff25; margin-top: var(--space-2); }
  .admin-sidebar[data-menu-ready] .admin-navigation { display: none; }
  .admin-sidebar[data-menu-ready].admin-sidebar--open .admin-navigation { display: flex; }
  .admin-side-nav { flex: none; overflow: visible; }
  .admin-side-nav__group { margin-bottom: var(--space-3); }
  .admin-side-nav__link, .admin-side-nav__sublink { min-height: 2.75rem; white-space: normal; }
  .admin-sidebar__foot { min-width: 0; padding: var(--space-3) var(--space-4); display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
  .admin-sidebar__user { white-space: normal; overflow-wrap: anywhere; }
  .admin-env, .admin-sidebar__user { margin-bottom: 0; }
  .admin-content h1 { overflow-wrap: anywhere; }
}

/* Stat tiles for the admin overview. A row of numbers, not cards in a two-column grid - the point is
   comparing them at a glance, and the attention state is what draws the eye when it matters. */
.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: var(--space-3); }
.stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: var(--space-4); }
.stat__label { font-size: .8125rem; color: var(--text-muted); line-height: 1.3; }
.stat__value { font-size: 2rem; font-weight: 700; line-height: 1.1; margin-top: var(--space-2); font-variant-numeric: tabular-nums; }
.stat--attention { border-color: var(--danger-200); background: var(--danger-50); }
.stat--attention .stat__value { color: var(--danger-700); }

/* ---- Blazor framework UI -----------------------------------------------------------------------
   Moved here from MainLayout.razor.css, which was scoped CSS. Scoped styles only match elements
   carrying that component's generated attribute, so the display:none never applied to AdminLayout's
   own copy of #blazor-error-ui - leaving a permanently visible error bar on every admin page. A
   global rule matches both. Tokenised while it moved; the old lightyellow needed
   `color-scheme: light only` to survive dark mode, which the tokens make unnecessary. */

#blazor-error-ui {
  display: none;
  position: fixed; bottom: 0; left: 0; width: 100%; z-index: 1000; box-sizing: border-box;
  padding: .6rem 1.25rem .7rem;
  background: var(--warn-50); color: var(--warn-700);
  border-top: 1px solid var(--warn-200);
  box-shadow: 0 -1px 2px rgba(0,0,0,.2);
}
#blazor-error-ui .dismiss { cursor: pointer; position: absolute; right: .75rem; top: .5rem; }

/* The action in that bar. It carried a class with no rule anywhere, so it took the global link colour -
   a brand navy picked for the page ground - while sitting on the warning ground. Taking the bar's own
   foreground keeps the pair the one the warning tokens were chosen as, and the underline means it still
   reads as a link with colour doing none of the work (1.4.1). */
#blazor-error-ui .reload {
  color: inherit;
  text-decoration: underline;
  font-weight: 650;
  margin-inline-start: .25rem;
}

/* Numbers stay in the sans, and tabular, whatever the surrounding type. A serif figure in a total is
   decoration on a page where the number is the entire point, and proportional digits make a column of
   prices fail to line up. */
.stat__value, .offer__price, .summary__total, .stepper__value, .qty__value, .qty__total,
table.data td.num, table.data th.num, .num, code {
  font-family: var(--font-sans);
  font-variant-numeric: tabular-nums;
}
code { font-family: var(--font-mono); }

/* ---- programme accents -------------------------------------------------------------------------
   risefoundation.am gives each programme area its own accent, and a buyer handed over from bebras.am
   should still see something familiar. The accent slot is already the right shape for this: a fill
   plus its ink, never touching an interactive control.

   Fill and ink are always set together, because a dark accent needs light ink and a light one needs
   dark - the pair is the unit, and setting the fill alone is how a flag ends up unreadable.

   Where this may appear: .offer__flag and the handover banner. Where it may NOT: .btn--primary, any
   .steps state, any .badge variant. Those encode meaning or spend money, and the button that spends
   money must look the same however the buyer arrived - otherwise "which thing do I click" moves with
   the referrer, and AA has to be re-derived per source. */

/*  codzilla's #0f5e86 used to be the same value as --donate-500, and is now the only place it appears.
    That was always a coincidence rather than a dependency - this is codzilla.am's programme colour and
    the donate button's colour is risefoundation.am's giving red - so it did not follow donate to the
    brick and must not be "corrected" to it later. */
[data-source="bebras"]   { --accent-500: #e08526; --accent-700: #3a1c02; }  /* ink on fill 5.61:1 */
[data-source="codzilla"] { --accent-500: #0f5e86; --accent-700: #eaf3f8; }  /* 6.30:1 */
[data-source="stories"]  { --accent-500: #8e4708; --accent-700: #fdf4e6; }  /* 6.30:1 */
/* A direct visit keeps the foundation's own amber from :root - 5.7:1. */

/* The handover banner used to reuse .env-banner--production, which in the admin panel means "real
   money mode". One class, two unrelated meanings, on the two surfaces where being misread matters
   most. */
.env-banner--handover {
  background: var(--brand-50);
  color: var(--brand-700);
  border-bottom: 1px solid var(--brand-200);
}

/* ---- language switcher ------------------------------------------------------------------------ */

/*  A globe button that opens a short menu, matching risefoundation.am.

    Built on <details>/<summary>, so it works with no JavaScript at all - which matters because Terms,
    Privacy, Error and NotFound render statically and never get a Blazor circuit. */

.lang-switch {
    position: relative;
    flex-shrink: 0;
}

.lang-switch__button {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.5rem 0.625rem;
    border-radius: var(--radius-sm, 6px);
    color: var(--ink-500);
    cursor: pointer;
    user-select: none;
    line-height: 1;
    list-style: none;          /* Firefox */
}

/*  The default disclosure triangle, removed in both spellings browsers use for it. */
.lang-switch__button::-webkit-details-marker { display: none; }
.lang-switch__button::marker { content: ""; }

.lang-switch__button:hover,
.lang-switch__button:focus-visible {
    color: var(--brand-700);
    background: var(--ink-50);
}

.lang-switch[open] > .lang-switch__button {
    color: var(--brand-700);
    background: var(--ink-50);
}

.lang-switch__globe {
    width: 1.25rem;
    height: 1.25rem;
    flex-shrink: 0;
}

.lang-switch__current {
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
}

.lang-switch__menu {
    position: absolute;
    top: calc(100% + 0.375rem);
    right: 0;                  /* Right-aligned: the switcher sits at the end of the header, and a
                                  left-aligned menu would hang off a narrow screen. */
    z-index: 40;
    min-width: 11rem;
    padding: 0.25rem;
    background: var(--surface);
    border: 1px solid var(--ink-200);
    border-radius: var(--radius-sm, 6px);
    /*  --shadow-lg, not a hardcoded pair. The literal here was mixed for a white ground and stayed
        that way in dark mode, where a 12%-black shadow against a #12171a page is invisible - so the
        menu floated over the page with nothing lifting it off. The token is defined in both themes and
        this was the only place that needed it, which is why it had been used exactly nowhere. */
    box-shadow: var(--shadow-lg);
}

.lang-switch__option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.5rem 0.625rem;
    border-radius: var(--radius-sm, 6px);
    font-size: 0.875rem;
    color: var(--ink-700);
    text-decoration: none;
    white-space: nowrap;
}

.lang-switch__option:hover,
.lang-switch__option:focus-visible {
    color: var(--brand-700);
    background: var(--ink-50);
    text-decoration: none;
}

/*  Marked by weight and a tick as well as colour - never by colour alone. */
.lang-switch__option.is-current {
    color: var(--brand-700);
    font-weight: 600;
}

.lang-switch__tick {
    width: 1rem;
    height: 1rem;
    flex-shrink: 0;
}

@media (max-width: 40rem) {
    .lang-switch__button {
        padding: 0.5rem;

        /*  44px, which is the smallest comfortable tap target. Measured at 36x36 on a phone: the label
            is hidden below, leaving the globe alone, and the padding that suited a button with text
            beside it left the target under a thumb's width. */
        min-width: 2.75rem;
        min-height: 2.75rem;
        justify-content: center;
    }

    .lang-switch__current {
        display: none;         /* The globe alone on a phone; the menu still names every language. */
    }

    /*  The menu opened OFF THE SIDE OF THE SCREEN, so a visitor on a phone could not change language at
        all. Measured at 390px: left=-80, right=96 - hanging off the LEFT edge, which is the opposite of
        what the right-alignment above was guarding against.

        The cause is an assumption that stops being true when the header wraps. .site-header__inner is a
        flex row with flex-wrap, so on a narrow screen the actions drop to a second line and start at the
        LEFT margin. `right: 0` then means "the right edge of a switcher sitting at x=96", and a 176px
        menu from there reaches x=-80.

        The fix is to stop anchoring to the switcher. Making .lang-switch static hands the menu's
        containing block to .site-header__inner, so `right` is measured from the HEADER's edge - which is
        the screen's edge, whatever row the switcher happened to wrap onto. The vertical anchor becomes
        the bottom of the header, which on a phone is where a dropdown from a wrapped toolbar belongs
        anyway.

        The max-width is the belt to that braces: even anchored correctly, a menu wider than the screen
        would run off the other side, and 11rem of min-width is close enough to a 320px phone to be worth
        stating. */
    .site-header__inner { position: relative; }
    /* Only the standard header supplies this containing block. Other headers, including the
       marketing page, keep the menu anchored to their language button. */
    .site-header__inner .lang-switch { position: static; }

    .site-header__inner .lang-switch__menu {
        top: calc(100% + 0.25rem);
        right: var(--space-3);
        left: auto;
    }

    .lang-switch__menu { max-width: calc(100vw - 1.5rem); }

    /*  A row in that menu is the control being aimed at, and 38px was under the mark. */
    .lang-switch__option { min-height: 2.75rem; }
}

/* ---- payment network marks --------------------------------------------------------------------

   Sized against Visa's published In-Store Brand Standards (September 2025), which govern anywhere a
   consumer encounters Visa and are the rules a vPOS review checks:

     - "must be displayed at a minimum width of 17.50 mm / 50 pixels"
     - "A clear space, free of any text or graphics, of .5X where X = height of the Visa Brand Mark
        must be maintained around all sides"
     - "must appear in a size that is at least as large as every other payment brand mark"
     - "Scale the Visa Brand Mark as needed but do not alter the proportions or elements in any way"

   Hence 2rem and not the 1.5rem this started at. The Visa mark is about 1.67:1, so 1.5rem tall put it
   at 40px wide - under the 50px floor. At 2rem it renders ~53px. Height rather than width is set
   because all three marks then share one baseline, which is what satisfies "at least as large as every
   other" without special-casing each file's aspect ratio.

   width:auto so a mark is never stretched, and no greyscale filter: Visa asks for Visa Blue wherever
   possible, and desaturating the marks would alter them. */
.paynets {
    display: flex;
    align-items: center;

    /* .5X clear space, where X is the mark height above. Applied as both gap and padding so the rule
       holds between the marks and around the outside of the group. */
    gap: 1rem;
    padding: 1rem 0 0;
    flex-wrap: wrap;
    margin: var(--space-3) 0 0;
    list-style: none;
}

.paynets__mark {
    display: block;
    height: 2rem;
    width: auto;
}

/*  Mastercard's official mc_symbol.svg carries its own transparent margin: the interlocking circles fill
    only 66.7% of the viewBox height and 77.6% of its width (measured, not guessed). Rendered at the same
    box height as the others it would therefore look a third smaller than them, and Visa's standards
    require the Visa mark be "at least as large as every other payment brand mark" - a comparison a
    reviewer makes by eye, on the visible mark, not on a bounding box.

    So the box is scaled up to put the visible circles on the same 2rem baseline: 2rem / 0.667 = 3rem.
    Scaling is what Mastercard's own guide permits ("use our provided files" - the file here is
    byte-identical to their download); cropping the viewBox to remove the padding would not be. */
.paynets__mark--mc {
    height: 3rem;
}

/* ---- product images ---------------------------------------------------------------------------- */

/*  A fixed aspect ratio, so a catalogue of covers photographed at different sizes still lines up and
    the page does not reflow as each one loads. Portrait, because a book is.

    4/5, because that is the ratio the covers are actually produced at. Every file in covers/ - the 2024
    and 2025 books, the three-book bundle and the Bebras Online entry - is 800x1000, and so is every
    blob the live shop is serving today, read back from their WebP headers rather than assumed. 3/4 was
    near enough to look right without being right: measured at 1440px it left a 12.14px band of
    --ink-50 above and below EVERY cover, 6.25% of the box, which on the light artwork of the book
    covers reads as the picture floating in a grey box a size too tall. Matching the box to the source
    takes that to 0.

    object-fit: contain rather than cover: a book cover cropped to fill a box loses its title, which is
    the one thing on it that matters. With the box and the source now the same shape it crops and pads
    nothing at all, and it still protects a cover that arrives some other shape - that one gets bands
    rather than a cover with its title cut off. The BOX never changes size either way, which is what the
    row's alignment actually depends on. */
.product__image {
    display: block;
    width: 100%;
    aspect-ratio: 4 / 5;
    object-fit: contain;
    background: var(--ink-50);
    border-radius: var(--radius-sm, 6px);
    margin-bottom: 0.75rem;
}

/*  What a product with no cover gets. A named tile rather than a broken-image icon or a gap, so a
    half-photographed catalogue looks unfinished rather than broken. */
.product__image--empty {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    text-align: center;
    font-size: 0.875rem;
    line-height: 1.4;
    /*  --text-muted rather than --ink-400: this is the product's name, standing in for a cover that
        does not exist, so it is body text and 1.4.3 asks 4.5:1 of it. --ink-400 gave 3.70:1 light and
        4.05:1 dark. The dashed edge stays quiet - it is decoration, not a boundary you operate. */
    color: var(--text-muted);
    border: 1px dashed var(--ink-200);
}

/* ---------- Sortable table headings ----------------------------------------- */

/*  The whole heading is the target, not just the words in it. A 13px uppercase label is already a small
    thing to hit; giving the button the cell's own padding makes the target the size of the cell, which
    is what a person aiming at a column heading expects to be able to click. */
.sortable { padding: 0 !important; }

/*  The row that holds the sort button and, where there is one, the hint mark beside it. The button
    takes the space so the target stays the width of the cell; the mark keeps its own. */
.sortable__cell { display: flex; align-items: center; }
.sortable__button {
    display: flex; align-items: center; gap: var(--space-2);
    flex: 1; min-width: 0; padding: .5rem var(--space-4);
    background: none; border: 0; cursor: pointer;
    font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
    text-align: start;
}

/*  A numeric heading sits over end-aligned figures, so it is end-aligned too - the arrow then lands
    beside the last digit rather than a column-width away from it. */
.sortable.num .sortable__button { justify-content: flex-end; }
.sortable.num .sortable__cell { justify-content: flex-end; }
.sortable__button:hover { background: var(--ink-100); color: var(--ink-700); }
.sortable--on .sortable__button { color: var(--ink-700); }

/*  The arrow is what says which way, so it must not be the first thing to disappear when the column is
    narrow. It never shrinks and never wraps away from its label. */
.sortable__button .icon { flex: none; }

/* ---------- Filter bars ----------------------------------------------------- */

/*  A panel, not a row of loose controls.

    It was three widgets floating between the heading and the table with nothing grouping them and
    nothing naming them - so it read as three unrelated settings, and the checkbox, whose label was the
    longest text in the strip, read as a preference rather than a filter. A tinted bordered box with a
    legend on it says "these narrow the list below" before any individual label is read.

    Wraps rather than scrolls: on a narrow admin window these belong on two lines, and a filter bar that
    scrolls sideways hides the filter that is switched on. */
.filters {
    display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-3) var(--space-4);
    margin-bottom: var(--space-4);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--ink-50);
}

/*  Everything in this bar lines up on the bottom edge of its control, and NOTHING gets there with a
    hand-tuned nudge.
    ------------------------------------------------------------------------------------------------
    The first version did it with padding-bottom on whichever items looked wrong, and the result was
    exactly what you would expect: the search box on the orders page sat seventeen pixels above the
    select and the checkbox beside it, because two of the three had been nudged and one had not. A
    layout held together by per-element corrections is one that comes apart the moment a control is
    added, and one was.

    The rule instead: align-items: flex-end on the container, so every child's BOTTOM meets. That works
    already for anything whose box is a control - a field with a label above it, a search box wrapping an
    input - because their bottom edge IS the control's bottom edge. The three items that are bare text or
    a bare checkbox are given the same vertical padding and transparent border an input has, so their
    boxes come out the same height and their bottoms land in the same place.

    Same height by construction, not by measurement. If the input padding below ever changes, this
    follows it. */
.filters__legend,
.filters__check,
.filters__count {
    padding-block: .5625rem;
    border-block: 1px solid transparent;
}

.filters__legend {
    display: inline-flex; align-items: center; gap: var(--space-2);
    font-size: .8125rem; font-weight: 650; text-transform: uppercase; letter-spacing: .03em;
    color: var(--text-muted);
}

/*  Each control gets a small label of its own. That is what lets the labels be SHORT: «Միայն վաճառքում»
    is unambiguous under a heading that says these are filters, where on its own it had to be
    «Միայն վաճառքում եղածները» and became the widest thing in the strip. */
.filters__field { display: flex; flex-direction: column; gap: .25rem; min-width: 0; }
.filters__field--grow { flex: 1 1 18rem; }
.filters__label { font-size: .75rem; color: var(--text-muted); }
/*  By element, because this codebase styles inputs by element rather than through a .input class - and
    a select is sized to its longest option by default, which in a flex row means the row's width is
    decided by whichever option happens to be wordiest. */
.filters__field input,
.filters__field select { width: 100%; max-width: none; min-width: 0; }

/*  A checkbox and its label read as one control, so the pair gets the hover target and the alignment
    rather than the tiny box alone. */
.filters__check input[type=checkbox] { flex: none; }

/*  A page that drops a bare control into the bar - the orders status list does - still lines up, because
    flex-end meets its bottom edge like everything else. It only needs to stop being stretched. */
.filters > select,
.filters > .checkbox { align-self: flex-end; }

.filters__check { display: inline-flex; align-items: center; gap: var(--space-2); }

/*  The other four admin pages label their search box with a magnifier inside it rather than with a word
    above it. Kept, rather than rewritten to match: the icon-in-box form is a perfectly good label, four
    pages already use it, and a rewrite of four files to change where a label sits is a lot of risk for a
    small gain. It picks up the panel's ground and alignment either way, which was most of the point.

    The magnifier sits INSIDE the field, so the bar does not grow a column of loose icons, and the input
    is padded to clear it - without which the first two characters typed sit under the icon. */
.filters__search { position: relative; display: inline-flex; align-items: center; }
.filters__search .icon { position: absolute; inset-inline-start: .625rem; color: var(--text-muted); pointer-events: none; }
.filters__search input { padding-inline-start: 2.125rem; min-width: 16rem; }

.filters__count {
    display: inline-flex; align-items: center; gap: var(--space-2);
    margin-inline-start: auto;
    font-size: .8125rem; color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/*  A way back out, offered only while something is hidden - which is the only moment it means anything,
    and the moment somebody has lost track of which of three filters is still on. */
.filters__clear {
    padding: 0; background: none; border: 0;
    font: inherit; color: var(--brand-600); text-decoration: underline; cursor: pointer;
}
.filters__clear:hover { color: var(--brand-700); }

/* ---------- Inline explanation ---------------------------------------------- */

/*  A small "i" next to a label whose meaning is not on the screen - a column of tax codes, a checkbox
    that changes a different page. The page-level help panel explains the workflow; this explains one
    word, at the moment it is being read, without opening anything.

    It carries its text in an aria-label as well as a title, because title alone is invisible to a
    keyboard and to a screen reader, and this admin panel has exactly one user who will not remember what
    "ԱՏԳ" abbreviates a year from now. */
.hint-mark {
    display: inline-grid; place-items: center;
    width: 1.5rem; height: 1.5rem; padding: 0; margin-inline-start: .25rem;
    background: none; border: 0; border-radius: 50%;
    color: var(--text-muted); cursor: help; vertical-align: -0.3em;
}
.hint-mark:hover, .hint-mark:focus-visible { color: var(--ink-700); background: var(--ink-100); }

/* ---------- Narrow admin tables --------------------------------------------- */

/*  Columns an operator reads but never acts on - a tax code, a department number, a device serial - are
    the ones that push an admin table past the window and start it scrolling sideways. A horizontal
    scrollbar is not a neutral cost: the first column scrolls out of view, so every row loses the one
    piece of information that says which row it is.

    So reference columns get a ceiling and wrap inside it, rather than setting the table's width. */
/*  The rule that makes a sideways scrollbar impossible, rather than merely unlikely.
    ------------------------------------------------------------------------------------------------
    Every attempt before this one was a ceiling on a particular column - 9rem here, 12rem there - and a
    ceiling cannot hold, because a column can never be narrower than the longest UNBREAKABLE run inside
    it. An order number is one token. So is an email address, a certificate subject, a ՊԵԿ error string,
    a URL. Give a cell max-width: 9rem and a 40-character token in it still demands 40 characters; the
    table grows past the window, .table-wrap starts scrolling, and the first column - the one that says
    which row you are looking at - goes off the left edge.

    overflow-wrap: anywhere is different in kind. It changes the cell's MIN-CONTENT width to a single
    character, so there is no longest run any more and the table can always be squeezed into whatever it
    is given. Not break-word, which looks identical here but leaves min-content at the whole token and
    would fix nothing. And it breaks a word only when the word does not fit - ordinary Armenian prose in
    a wide column still wraps between words, exactly as before.

    With this in place .table-wrap's overflow-x: auto never fires on an admin table. It stays as the
    backstop for the buyer-facing basket, which is a different table on a phone. */
.admin-content table.data th,
.admin-content table.data td { overflow-wrap: anywhere; }

/*  Ceilings and floors, now doing the job they are actually good at: proportion. Not fitting - the rule
    above does that - but keeping a reference column from taking space the name column needs. */
.admin-content table.data .col-ref { max-width: 9rem; white-space: normal; }

/*  10rem, not 12. Measured: the products table wanted 1172px of a 1054px container and was still
    scrolling after the wrap rule above, because a floor is a demand like any other - this column's alone
    was 12rem plus 2rem of padding. 10rem still holds a two-word Armenian title on one line and gives the
    table back the 32px it was over by. Floors on a table that must fit are a budget, not a preference. */
.admin-content table.data .col-name { min-width: 10rem; max-width: 18rem; }

/*  A product type is a short known label - there are five of them and «Առաքման ծառայություն» is the
    longest. The blanket overflow-wrap: anywhere above was folding it into five lines of four letters,
    which made that row 137px tall against 65 for every other. nowrap is safe here precisely because the
    set is closed: this column cannot be surprised by its content the way a name or an error string can. */
.admin-content table.data .cell-type { white-space: nowrap; }

/*  A description belongs on ONE line in a table.

    It was rendered in full under the product name, so a two-sentence description turned that row four
    lines tall while every other row was one - and a table of wildly uneven rows is harder to scan than
    a table with a column missing, because the eye loses the horizontal. Clamped to a line with an
    ellipsis, and the full text is on the element's title for a hover and in the edit panel for reading.

    The max-width on .col-name above is what makes this work: an ellipsis needs a boundary to be
    ellipsed against, and in an auto-layout table a cell with no ceiling simply grows instead. */
.admin-content table.data .cell-note {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/*  Numbers stay on one line - a price broken across two lines is unreadable - and line up under each
    other so a column of them can be compared by eye. */
.admin-content table.data .col-num,
.admin-content table.data .num { white-space: nowrap; text-align: end; font-variant-numeric: tabular-nums; }

/* ---------- Field groups ---------------------------------------------------- */

/*  A <fieldset> around a set of fields that belong to one decision - the tax classification, the bounds
    on a donation, the contents of a bundle. The element is the semantically right one (a screen reader
    announces the legend before each field inside it, so "ԱՏԳ ծածկագիր" is heard as part of "ՀԴՄ
    դասակարգում" rather than free-floating), but its default border draws a box around the legend text
    and inherits none of this page's spacing, so it needs saying explicitly. */
.fieldset { border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-4); margin: var(--space-5) 0 0; }
.fieldset > legend { padding: 0 var(--space-2); font-weight: 650; font-size: .9375rem; }

/*  The row being edited, so the panel below is visibly attached to something. Marked with a border on
    the leading edge rather than a background: the table already uses background for the hover state, and
    two meanings on one property is how a row ends up looking selected because a cursor passed over it. */
.admin-content table.data tr.is-editing > td { background: var(--brand-50); }
.admin-content table.data tr.is-editing > td:first-child { box-shadow: inset 3px 0 0 var(--brand-500); }


/*  Status pills wrap in an admin table, and only there.

    .badge is nowrap everywhere else and should stay that way: on a product card or beside a heading a
    pill broken over two lines reads as two pills. But «Վճարման սպասում» and «Մասնակի վերադարձ» are the
    longest unbreakable runs in the orders table, and a column can never be narrower than its longest
    unbreakable run - so those two labels alone were setting a floor that put the whole table past the
    window and started it scrolling sideways. The first column then scrolls out of view, and every row
    loses the one thing that says which row it is.

    Two lines in a pill costs a little vertical space in one table. A horizontal scrollbar costs the
    order number on every row. */
/*  A pill wraps between words and NEVER inside one.

    Two rules, and both are needed. white-space: normal undoes the nowrap that badges carry everywhere
    else, which was pinning «Վճարման սպասում» to one line and setting a floor under the whole table.
    overflow-wrap: normal then opts back out of the blanket "break anywhere" rule for admin cells - which
    had turned «Թեստային» into «Թեստայ / ին», a word split down the middle inside a coloured pill.

    Between words is a line break. Inside a word is a typo. */
.admin-content table.data .badge { white-space: normal; overflow-wrap: normal; }

/*  An identifier is one run of characters, always. Order numbers, receipt numbers, anything read down a
    phone or matched against a bank statement - "RPF-2026-" above "000900" is two strings, and somebody
    will eventually copy one of them. */
.admin-content table.data .cell-id { white-space: nowrap; }

/* ---------- Modal dialog ------------------------------------------------------------------------ */

/*  A form that opens over the row it belongs to, rather than below the table.

    Sized from the viewport, not from its contents: 90vh means a long form scrolls INSIDE the dialog
    while the page behind stays where it was, so closing an edit puts you back on the row you were
    editing rather than at the top of a list you had just filtered. */
.modal {
    width: min(48rem, calc(100vw - 2rem));
    max-height: min(90vh, 60rem);
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--text);
    box-shadow: 0 24px 48px -12px rgb(0 0 0 / .25);
    overflow: hidden;
}

/*  Roomier for a form with a table in it - the bundle editor has five columns of its own. */
.modal--wide { width: min(64rem, calc(100vw - 2rem)); }

/*  ::backdrop does NOT inherit from the page, so a token used here resolves to nothing and the backdrop
    comes out transparent - which looks like the dialog is floating over a page that is still live. A
    literal, deliberately. */
.modal::backdrop { background: rgb(15 23 42 / .55); }

/*  Grid rather than flow, so the body is the only part that scrolls and the heading stays put. */
.modal { display: grid; grid-template-rows: auto 1fr; }

.modal__head {
    display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4);
    padding: var(--space-4) var(--space-5);
    border-bottom: 1px solid var(--border);
}
.modal__title { margin: 0; font-size: 1.0625rem; }

.modal__close {
    display: inline-grid; place-items: center;
    width: 2.25rem; height: 2.25rem; flex: none; padding: 0;
    background: none; border: 1px solid transparent; border-radius: var(--radius-sm);
    color: var(--ink-700); cursor: pointer;
}
.modal__close:hover { background: var(--ink-100); border-color: var(--border-control); }

.modal__body { padding: var(--space-5); overflow-y: auto; }

/*  The form's own action bar, stuck to the bottom of the scroll area.

    Sticky inside the body rather than a row of the dialog's grid, because a submit button has to be
    inside the <form> it submits - see the comment in ModalDialog. It lands in the same place either way;
    only one of the two routes can be tested.

    The negative margins pull it out to the body's padding edge so its background spans the full width,
    and the bottom one closes the gap under it. */
.modal__actions {
    position: sticky; bottom: 0;
    display: flex; gap: var(--space-3); align-items: center;
    margin: var(--space-5) calc(var(--space-5) * -1) calc(var(--space-5) * -1);
    padding: var(--space-4) var(--space-5);
    border-top: 1px solid var(--border);
    background: var(--ink-50);
}


/* ---------- Product thumbnails ------------------------------------------------------------------ */

/*  A thumbnail beside the name, in the name's own cell.

    Fixed width AND height with object-fit: cover, so a portrait cover and a landscape one occupy the
    same square and every row stays the same height. An unconstrained image in a table cell is the
    fastest route to a four-hundred-pixel row - which is the complaint the descriptions had just caused.

    flex: none on the image and min-width: 0 on the text is the pair that makes the text column the one
    that shrinks. Without the second, a long unbreakable name pushes the thumbnail out of the cell. */
.cell-titled { display: flex; gap: var(--space-3); align-items: flex-start; }
.cell-titled__text { min-width: 0; }

.cell-thumb {
    flex: none;
    width: 2.5rem; height: 2.5rem;
    object-fit: cover;
    border-radius: var(--radius-sm);
    background: var(--ink-100);
}

/*  "No picture" occupying exactly the space a picture would, so a list of mostly-imageless products does
    not have its names jolting a few pixels sideways row by row. */
.cell-thumb--none {
    display: grid; place-items: center;
    background: none;
    border: 1px dashed var(--border-control);
    color: var(--text-muted);
}

/* ---------- The image field in the edit dialog --------------------------------------------------- */

.image-edit { display: flex; gap: var(--space-4); align-items: flex-start; flex-wrap: wrap; }

/*  Big enough to judge, small enough not to own the form. A shop card is square, so this is square. */
.image-edit__preview {
    width: 7.5rem; height: 7.5rem; flex: none;
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--ink-50);
    overflow: hidden;
}
.image-edit__preview img { width: 100%; height: 100%; object-fit: cover; display: block; }

.image-edit__none {
    display: grid; place-items: center; gap: var(--space-1);
    width: 100%; height: 100%;
    color: var(--text-muted); font-size: .8125rem; text-align: center;
}

.image-edit__controls { display: flex; flex-direction: column; gap: var(--space-3); align-items: flex-start; min-width: 0; }


/*  A cell holding a list, capped by LINES rather than by width.

    The distinction is the whole point. A max-width on a cell full of long words does not make the table
    narrower in a useful way - it makes that one column a vice: the words are allowed to break mid-syllable
    and denied any room, so a three-item order rendered as twenty lines of four letters. Capping the line
    count instead lets the column take a sensible width and stops the ROW getting tall, which is what was
    actually wrong with it.

    -webkit-line-clamp is the only cross-browser way to do this; it is unprefixed nowhere useful yet, and
    the -webkit- form is honoured by every current engine including Firefox. The `display: -webkit-box` it
    requires is why this is on an inner div rather than on the <td> - a table cell cannot be a webkit-box
    and remain a table cell. */
.admin-content table.data .cell-lines {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;

    /*  And the words in it wrap between words again. The blanket overflow-wrap: anywhere on admin cells
        is what guarantees the table always fits; here there is a line cap doing that job instead, so the
        text can go back to breaking where Armenian breaks. */
    overflow-wrap: break-word;
}


/*  The far end of a modal's action bar.

    A destructive action belongs away from the one somebody came to press. Save is on the left where the
    eye lands; delete is pushed to the opposite edge, so reaching it is a deliberate movement rather than
    a near miss. */
.modal__actions__end { display: flex; align-items: center; gap: var(--space-3); margin-inline-start: auto; }

/*  Quiet until it is the thing being done. A permanently red button in a form somebody opens to fix a
    typo reads as a warning about the form, so this stays a secondary button wearing danger's colour on
    its text, and only the CONFIRMATION - after a deliberate press - is a filled danger button. */
.btn--danger-text { color: var(--danger-700); }
.btn--danger-text:hover:not(:disabled) { background: var(--danger-50); border-color: var(--danger-200); }


/* ---------- Tap targets on a phone -------------------------------------------------------------

   Measured on a 390px viewport with ops/measure-mobile.js, which is the only way any of this was going
   to be found: every one of these renders correctly, passes every test, and is simply too small for a
   thumb. 44px is the smallest comfortable target.

   Only on a phone. A pointer is precise and a 38px button under a mouse is fine; stretching every
   control to 44px on a desktop would make the admin panel a third taller for no one's benefit. */
@media (max-width: 40rem) {
    /*  The one that costs money. «Ավելացնել զամբյուղ» and «Գրանցել մասնակցին» measured 38px - the
        primary action on the shop, and the thing a visitor came to press. */
    .btn { min-height: 2.75rem; }

    /*  The header's own controls. The nav links measured 40px and the basket 40x40; both are reached
        constantly and neither had room around it. */
    .site-nav a,
    .site-header__basket { min-height: 2.75rem; display: inline-flex; align-items: center; }

    /*  The brand, at 168x32, is a link somebody taps to get home when they are lost - which is exactly
        when a small target is most annoying. */
    .site-header__brand { min-height: 2.75rem; }

    /*  A checkbox at 18px is a hard thing to hit, and one of these agrees to marketing while another
        makes a donation anonymous - neither is a box to tick by accident OR to miss repeatedly.
        24px is the WCAG 2.5.8 minimum; the LABEL is the real target and stays the full row, so this is
        the fallback for somebody aiming at the box itself.

        margin-block-start goes with it: the 3px nudge that aligned an 18px box with the first line of
        text leaves a 24px one sitting high. */
    .checkbox input[type=checkbox] { width: 1.5rem; height: 1.5rem; margin-block-start: 0; }

    /*  Footer links measured 22px. Not stretched to 44 - five stacked full-height links would make the
        footer taller than some of the pages above it - but padded to a size a thumb can find, which is
        what 2.5.8 actually asks of a link in a list. */
    .site-footer nav a { display: inline-block; padding-block: .5rem; }
}


/*  The hidden attribute beats a display rule.

    The browser's own stylesheet says [hidden] { display: none }, but it is a UA rule and any author
    rule setting display outranks it - .product__image is `display: block`, so an <img hidden> inside a
    card stayed perfectly visible. That is a trap rather than a preference: hidden is the attribute
    every fallback here toggles, and a hidden thing that shows anyway is worse than no fallback.

    !important because the point is to be unconditional. */
[hidden] { display: none !important; }

/* ---------- The header on a phone --------------------------------------------------------------

   It wrapped onto two rows - brand on the first, the basket and the globe alone on a second - which
   reads as a mistake rather than as a layout and spends a fifth of a small screen saying nothing. One
   row now: brand, then language, basket and the menu, with the two nav links inside it. */

/*  Desktop: no menu button, because the links are already on the row. */
.site-menu { display: none; position: relative; }

.site-menu__button {
    display: inline-grid; place-items: center;
    width: 2.75rem; height: 2.75rem;
    border-radius: var(--radius-sm); color: var(--ink-500); cursor: pointer;
    list-style: none;
}
.site-menu__button::-webkit-details-marker { display: none; }
.site-menu__button::marker { content: ""; }
.site-menu__button svg { width: 1.375rem; height: 1.375rem; }
.site-menu[open] > .site-menu__button { color: var(--brand-700); background: var(--ink-50); }

/*  The panel drops from the bottom of the HEADER rather than from the button, for the same reason the
    language menu does - see the note there. Full width, because a phone has no room for a floating card
    and a full-width row is a better target than a narrow one. */
.site-menu__panel {
    position: absolute;
    top: 100%;
    inset-inline: 0;
    z-index: 40;
    display: flex; flex-direction: column;
    padding: var(--space-2);
    background: var(--surface);
    border-block-start: 1px solid var(--border);
    box-shadow: var(--shadow-lg);
}
.site-menu__panel a {
    padding: var(--space-3);
    border-radius: var(--radius-sm);
    color: var(--ink-700); text-decoration: none; font-weight: 500;
    min-height: 2.75rem; display: flex; align-items: center;
}
.site-menu__panel a.active { background: var(--brand-50); color: var(--brand-700); }

@media (max-width: 40rem) {
    /*  The links move into the menu, so the row stops wrapping. */
    .site-nav { display: none; }
    .site-menu { display: block; position: static; }

    /*  Static, so the panel's containing block is the header - see .lang-switch, same fix, same reason:
        a panel anchored to a control that has wrapped is a panel anchored to the wrong place. */
    .site-header__inner { flex-wrap: nowrap; gap: var(--space-2); }
    .site-header__brand { min-width: 0; }
    .site-header__name { min-width: 0; }
    .site-header__actions { margin-inline-start: auto; flex: none; }

    /*  Around 28px. It was the most dominant thing on the screen by some way, on a page whose job is to
        offer three choices - the choices should carry the page, not the question above them. */
    .page > .page-header h1, .page h1 { font-size: 1.75rem; }

    /*  A more compact footer: the links were the tallest thing in it and the gaps were set for a desktop
        row that no longer exists at this width. */
    .site-footer { padding: var(--space-5) var(--space-4); }
    .site-footer__inner { gap: var(--space-3); }
}

/* ---------- Cards that look like somewhere to go ------------------------------------------------ */

/*  The chevron, and the row it sits in. Without it three bordered rectangles holding a title and a line
    of grey text read as notices rather than as the three things this page exists to offer. */
.choice { flex-direction: row; align-items: center; gap: var(--space-4); }
.choice__text { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.choice__go {
    width: 1.25rem; height: 1.25rem; flex: none;
    margin-inline-start: auto;
    color: var(--text-muted);
    transition: transform .12s ease, color .12s ease;
}
.choice:hover .choice__go { color: var(--brand-600); transform: translateX(2px); }

/* ---------- Hover is for things that hover ------------------------------------------------------

   A touch browser leaves :hover applied after a tap until something else is tapped, so returning to
   this page from the first card left that card - and only that card - wearing a turquoise border, which
   reads as a selected state that does not exist. All three cards have identical CSS; the difference was
   entirely a finger.

   Scoped rather than removed: on a pointer the lift is a genuine affordance. `hover: hover` is the query
   for "this input can actually hover", and it is false on a touchscreen. */
@media (hover: none) {
    .choice:hover {
        border-color: var(--border);
        box-shadow: var(--shadow-sm);
    }
    .choice:hover .choice__go { color: var(--text-muted); transform: none; }
}
