/* ═══════════════════════════════════════════════════════════════════════
   MeducateED — Components: nav, footer, buttons, cards, badges, forms,
   Corvus widget, custom cursor. Shared across all public pages.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Buttons ─────────────────────────────────────────────────────────── */
.btn {
    display: inline-flex; align-items: center; gap: var(--s-2);
    font-family: var(--font-subhead); font-weight: 400; font-size: .82rem;
    letter-spacing: .04em; padding: .85rem 1.6rem; border-radius: var(--radius);
    border: 1px solid transparent; transition: transform var(--dur) var(--ease), background .3s, color .3s, border-color .3s;
    text-transform: uppercase;
}
.btn:hover { transform: translateY(-2px); }
.btn-gold { background: var(--gold-live); color: #fff; }
.btn-gold:hover { background: #FFDAA4; color: #ECA424; }
.btn-outline { border-color: currentColor; color: var(--ink); }
.on-ink .btn-outline { color: var(--paper); }
.btn-outline:hover { background: var(--ink); color: var(--gold-live); }
.on-ink .btn-outline:hover { background: var(--paper); color: var(--gold-live); }
.btn-ghost { color: var(--gold-live); padding-inline: 0; }
.btn-lg { padding: 1.1rem 2.2rem; font-size: 1rem; }
.btn-link { color: var(--teal); font-weight: 500; }
.on-ink .btn-link { color: var(--gold-live); }

/* ── Badges ──────────────────────────────────────────────────────────── */
.badge {
    display: inline-block; font-family: var(--font-mono); font-size: .64rem;
    letter-spacing: .12em; text-transform: uppercase; padding: .3em .7em;
    border-radius: 3px; border: 1px solid currentColor;
}
.badge-live { color: var(--green); }
.badge-soon { color: var(--teal-lt); }
.badge-gold { color: var(--gold); background: rgba(236,164,36,.1); border-color: transparent; }

/* ── Cards ───────────────────────────────────────────────────────────── */
.card {
    background: var(--paper); border: 1px solid var(--border); border-radius: var(--radius-lg);
    padding: var(--s-6); transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
    height: 100%;
    /* ⭐ The card is the query container for its own type (see the block at the
       end of this file). Without this, a heading sized in `vw` grows with the
       WINDOW inside a track that is a third of it, and gets clipped at the
       card edge — measured +127px on /academics/disciplines at 1024px. */
    container-type: inline-size;
    /* A grid/flex item defaults to min-width:auto and so refuses to shrink
       below its widest unbreakable content, pushing the whole track wide. */
    min-width: 0;
}
.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }
.card h3, .card h4 { margin-bottom: var(--s-3); }
.card p { color: var(--muted); font-size: .98rem; }
.card-media { aspect-ratio: 3/2; background: var(--ink3); border-radius: var(--radius); overflow: hidden; margin-bottom: var(--s-4); position: relative; }
.card-media img { width: 100%; height: 100%; object-fit: cover; }
.on-ink .card { background: var(--ink3); border-color: var(--border-dark); }
.on-ink .card p { color: var(--peach); }

/* 3D tilt hover (product cards) */
.card-3d { transform-style: preserve-3d; will-change: transform; }

/* ── Header / Nav ────────────────────────────────────────────────────── */
.site-header {
    position: sticky; top: 0; z-index: 100;
    background: rgba(2,15,36,.86); backdrop-filter: blur(14px);
    border-bottom: 1px solid var(--border-dark); color: var(--paper);
    transition: background .3s;
}
/* `gap` matters here: the logo is flex:0 0 auto (it must not deform), so with
   space-between alone the nav items had nothing stopping them running into it
   once the row got tight — which is exactly what happened at 1024px. */
.nav { display: flex; align-items: center; justify-content: space-between; gap: var(--s-5); height: 72px; }
/* flex-shrink:0 — at 1024px the nav row squeezed the wordmark by 17px on
   every page (measured), clipping the 'ED'. The nav items wrap/scroll
   before the brand is allowed to deform. */
.nav-logo { font-family: var(--font-wordmark); font-size: 1.55rem; font-weight: 400; letter-spacing: .01em; color: var(--paper); display: inline-flex; align-items: center; gap: .6rem; line-height: 1; flex: 0 0 auto; white-space: nowrap; }
.nav-logo em { color: var(--gold-live); font-style: normal; }
.nav-logo-mark { width: 40px; height: 40px; object-fit: contain; }
.nav-menu { display: flex; align-items: center; gap: var(--s-6); min-width: 0; }

/* The band between the mobile drawer (<=940px) and a comfortable desktop.
   Seven nav items plus a CTA plus the wordmark do not fit at 1024 at full
   size; tightening the row is better than shrinking the brand or dropping
   to the hamburger on a laptop. */
@media (min-width: 941px) and (max-width: 1180px) {
    .nav-menu { gap: var(--s-4); }
    .nav-link { font-size: .7rem; letter-spacing: .05em; }
    .nav-logo { font-size: 1.25rem; gap: .4rem; }
    .nav-logo-mark { width: 30px; height: 30px; }
    .nav-cta { margin-left: var(--s-2); padding-inline: 1rem; }
}
.nav-item { position: relative; }
.nav-link {
    font-size: .82rem; letter-spacing: .1em; text-transform: uppercase; font-weight: 400;
    color: var(--peach); padding: .5rem 0; transition: color .25s;
}
.nav-link:hover, .nav-item:hover .nav-link { color: var(--gold-live); }
.nav-cta { margin-left: var(--s-4); }

/* Mega menu */
.mega {
    position: absolute; top: 100%; left: 50%; transform: translateX(-50%) translateY(10px);
    min-width: 640px; background: var(--ink2); border: 1px solid var(--border-dark);
    border-radius: var(--radius-lg); padding: var(--s-6); box-shadow: var(--shadow-lg);
    opacity: 0; visibility: hidden; transition: opacity .3s, transform .3s; z-index: 120;
    display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s-5);
}
.nav-item.has-mega:hover .mega,
.nav-item.has-mega:focus-within .mega { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); }
.mega-col h5 { font-family: var(--font-mono); font-size: .68rem; letter-spacing: .16em; text-transform: uppercase; color: var(--gold); margin-bottom: var(--s-3); }
.mega-col a { display: block; color: var(--peach); font-size: .92rem; padding: .35rem 0; transition: color .2s, padding-left .2s; }
.mega-col a:hover { color: var(--gold-live); padding-left: 6px; }

/* ── The signed-in account menu (2026-09-17) ──────────────────────────────
   A <details>, so it needs no JavaScript and is keyboard accessible for free.
   ⛔ IT IS NOT `.mega`. That panel is `left:50%; translateX(-50%)` with a
      640px floor — correct for a centred nav item, and off the right edge of
      the viewport for something anchored to the LAST item in the bar. This one
      pins to its right edge and sizes to its content instead. */
.acct { position: relative; margin-left: var(--s-4); }
.acct > summary {
    list-style: none; cursor: pointer; display: flex; flex-direction: column;
    line-height: 1.15; padding: .3rem .6rem; border-radius: var(--radius);
    border: 1px solid transparent; transition: border-color .2s, background .2s;
}
/* Safari still paints the disclosure triangle without this. */
.acct > summary::-webkit-details-marker { display: none; }
.acct > summary:hover, .acct[open] > summary { border-color: var(--border-dark); background: rgba(255,255,255,.04); }
.acct-name { color: var(--paper); font-size: .92rem; font-weight: 600; }
.acct-id   { color: var(--gold); font-size: .66rem; letter-spacing: .08em; }
.acct-panel {
    position: absolute; top: 100%; right: 0; margin-top: 8px; min-width: 190px;
    background: var(--ink2); border: 1px solid var(--border-dark);
    border-radius: var(--radius-lg); padding: var(--s-3); box-shadow: var(--shadow-lg);
    z-index: 130; display: grid; gap: 2px;
}
.acct-panel a { display: block; color: var(--peach); font-size: .9rem; padding: .5rem .6rem; border-radius: var(--radius); transition: color .2s, background .2s; }
.acct-panel a:hover { color: var(--gold-live); background: rgba(255,255,255,.05); }

.nav-toggle { display: none; background: none; border: 0; color: var(--paper); font-size: 1.6rem; }

/* On desktop the clip wrapper must not exist in layout at all, or it would
   become the flex item and the nav would stop laying out as a row. */
.nav-drawer-clip { display: contents; }

/* Mobile nav */
@media (max-width: 940px) {
    .nav-menu {
        position: absolute; inset: 0; background: var(--ink); flex-direction: column;
        align-items: flex-start; gap: var(--s-4); padding: var(--s-8) var(--s-6);
        transform: translateX(100%); overflow-y: auto;
        /* ⭐ `visibility` does two jobs that transform alone cannot, and both are
           real bugs (measured 2026-09-14 with zz-probe at 330/390/768px):

           1. A closed drawer parked at translateX(100%) still occupies a box one
              viewport to the RIGHT, and Chromium counts it in
              documentElement.scrollWidth — every page reported scrollWidth =
              2x the viewport at <=768px. `body { overflow-x: hidden }`
              (global.css) hides that from the user on desktop, but Safari on iOS
              does not always honour that propagation, which is the classic cause
              of a marketing page that can be swiped sideways into a blank column.

           2. A visible-but-off-screen <nav> is still IN THE TAB ORDER and still
              read by screen readers. Before this, tabbing from the logo on a
              phone walked silently through ten invisible links.

           The delay on the way OUT lets the slide-out finish before it is
           hidden; on the way IN it must be instant, hence the .open override. */
        visibility: hidden;
        transition: transform .4s var(--ease), visibility 0s linear .4s;
    }

    /* ⛔ THE CLIP. Claim 1 in the comment above was measured FALSE on
       2026-09-14: with `visibility: hidden` applied from this very rule, a
       487px viewport still reported scrollWidth 974 and the page really
       scrolled right by 428px (scrollTo(9999,y) -> scrollX 428.8). Setting
       the drawer invisible does not take it out of the scrollable area.

       Nor can body clip it: the drawer is position:fixed, so its containing
       block is the viewport and `body { overflow-x: hidden }` never applies.
       And the two one-line cures both BREAK THE STICKY HEADER - with a
       control, header.site-header holds at top 0 without them and drops to
       -1078 under `html { overflow-x: clip }`.

       So the drawer gets its own clipping viewport: a fixed box of exactly
       the visible area (which therefore adds no scroll width of its own)
       with overflow:hidden, which DOES clip a fixed child because this box
       is that child's containing block. The slide animation is untouched. */
    .nav-drawer-clip {
        display: block;
        position: fixed;
        /* ⛔⛔ `bottom: 0` DOES NOT REACH THE BOTTOM OF THE SCREEN HERE, AND THAT
           IS WHY THE MOBILE MENU HAS ALWAYS BEEN A 96px STRIP.
           `.site-header` carries `backdrop-filter: blur(14px)`, and a
           backdrop-filter - exactly like `filter` or `transform` - makes an
           element THE CONTAINING BLOCK FOR ITS FIXED DESCENDANTS. So this box,
           and the drawer before it, resolve their insets against the 73px-tall
           HEADER, not against the viewport: `top: 72px; bottom: 0` inside 73px
           is a 1px box, and the drawer fell back to content height (measured
           487x96 with ten links inside a scrolling strip).
           An explicit height is the only thing that survives that, and it must
           be dvh so a phone browser chrome collapsing does not crop it. */
        top: 72px;
        left: 0;
        right: 0;
        bottom: auto;
        height: calc(100vh - 72px);
        height: calc(100dvh - 72px);
        overflow: hidden;
        z-index: 99;
        /* The clip box covers the page; only the drawer inside it may be
           clicked, or an invisible sheet would eat every tap. */
        pointer-events: none;
    }
    .nav-menu { pointer-events: auto; }
    .nav-menu.open {
        transform: translateX(0);
        visibility: visible;
        transition: transform .4s var(--ease), visibility 0s linear 0s;
    }
    .nav-toggle { display: block; }
    .mega { position: static; transform: none; min-width: 0; grid-template-columns: 1fr; opacity: 1; visibility: visible; display: none; background: transparent; border: 0; padding: var(--s-2) 0 0 var(--s-4); box-shadow: none; }
    .nav-item.has-mega.open .mega { display: grid; }
    .nav-cta { margin-left: 0; }
    /* In the drawer the panel flows instead of floating — an absolutely
       positioned menu inside a sliding drawer is how the mobile nav broke
       before (gotcha 527). */
    .acct { margin-left: 0; }
    .acct-panel {
        position: static; margin-top: var(--s-2); min-width: 0;
        background: transparent; border: 0; box-shadow: none;
        padding: 0 0 0 var(--s-4);
    }
}

/* ── Footer ──────────────────────────────────────────────────────────── */
.site-footer { background: var(--ink); color: var(--peach); padding-block: var(--s-12) 0; border-top: 1px solid var(--border-dark); }
.footer-grid { display: grid; grid-template-columns: 1.6fr repeat(4, 1fr); gap: var(--s-8); }
@media (max-width: 900px) { .footer-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .footer-grid { grid-template-columns: 1fr; } }
.footer-brand .nav-logo { color: var(--paper); }
.footer-brand p { margin-top: var(--s-4); font-size: .92rem; max-width: 34ch; }
.footer-col h5 { font-family: var(--font-mono); font-size: .66rem; letter-spacing: .16em; text-transform: uppercase; color: var(--gold); margin-bottom: var(--s-4); }
.footer-col a { display: block; color: var(--peach); font-size: .9rem; padding: .3rem 0; transition: color .2s; }
.footer-col a:hover { color: var(--gold-live); }
/* ⚠ margin-top was var(--s-10) — that was the gap in Mo’s screenshot.
   The rule now sits directly under the social/badge row. */
.footer-bottom { display: flex; justify-content: space-between; flex-wrap: wrap; gap: var(--s-4); margin-top: 0; padding-top: var(--s-5); border-top: 1px solid var(--border-dark); font-size: .82rem; }
.footer-bottom a { color: var(--peach); } .footer-bottom a:hover { color: var(--gold-live); }


/* ── Skyline band — site-wide, above the footer ───────────────────────────
   A continuous Cairo skyline scrolling behind the footer. Mo's requirement
   was "no space between pics ... literally 0", so read the three rules under
   .sky-track before changing any of them; each one closes a different way
   for a gap to reappear. */
.skyband {
    /* .site-footer has side padding via .container only, so this is already
       edge-to-edge. display:flex is REQUIRED — MedFX.marquee appends a clone
       of the track as a SIBLING and relies on flex to lay it end-to-end. */
    display: flex;
    overflow: hidden;
    /* (2) kills whitespace-between-tags even if the flex below is ever lost */
    font-size: 0;
    line-height: 0;
    /* One variable drives the band: the tiles are 800x240, so a tile's rendered
       width is always height x 10/3, and the overlap below is expressed in the
       same unit so it stays proportional at every breakpoint. */
    --sky-h: clamp(58px, 9vw, 110px);
    height: var(--sky-h);
    /* It now sits BELOW the footer contents, so the breathing room goes
       above it and the band closes the page. */
    margin-top: var(--s-8);
    margin-bottom: 0;
    position: relative;
    /* (3) the horizon. Painted by the BAND, so a tile that ends a pixel short
       still meets an unbroken ground line. */
    /* The horizon line now sits at the BASE of the page, under the city. */
    border-bottom: 2px solid var(--gold);
    /* Fade the ends into the footer instead of chopping the buildings off. */
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
            mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}

.sky-track {
    /* (1) THE RULE THAT MATTERS. flex + gap:0 = adjacent boxes, no gutters,
       and no inline whitespace between them. */
    display: flex;
    gap: 0;
    flex: 0 0 auto;
    height: 100%;
    will-change: transform;
}

.sky-track img {
    display: block;
    height: 100%;
    width: auto;
    /* Never let flex squeeze a tile — that is what would open a gap at the
       far end of the track. */
    flex: 0 0 auto;
    margin: 0;
    padding: 0;
    border: 0;
    /* Each tile's artwork TAPERS at its own left and right edges — the
       buildings get shorter towards the ends. Butting them at exactly 0
       therefore gives a layout with no gap but a visible DIP every 367px,
       which is not what "continuous skyline" means. Overlapping by ~22% of
       the band height tucks each tapered edge under its neighbour, so the
       roofline stays busy across the join and the seam disappears. It also
       makes the hairline sub-pixel seam impossible.
       Expressed in --sky-h, so the proportion holds at every size. */
    width: calc(var(--sky-h) * 10 / 3);   /* 800/240, the tile's own ratio */
    margin-inline-end: calc(var(--sky-h) * -0.22);
    opacity: .9;
}

/* Reduced motion: the marquee never starts (MedFX.marquee returns early), so
   show a still skyline rather than a band that looks broken. */
@media (prefers-reduced-motion: reduce) {
    .skyband { -webkit-mask-image: linear-gradient(90deg, #000 92%, transparent);
                       mask-image: linear-gradient(90deg, #000 92%, transparent); }
}

/* Phone: shorter band, and main.js already renders fewer tiles. */
@media (max-width: 560px) {
    .skyband { --sky-h: 52px; margin-top: var(--s-6); border-bottom-width: 1px; }
}

/* ── Forms ───────────────────────────────────────────────────────────── */
.field { margin-bottom: var(--s-5); }
.field label { display: block; font-size: .82rem; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin-bottom: var(--s-2); font-weight: 500; }
.on-ink .field label { color: var(--peach); }
.field .req { color: var(--red); }
.input, .textarea, .select {
    width: 100%; font: inherit; font-size: 1rem; padding: .8rem 1rem;
    background: var(--paper); border: 1px solid var(--border); border-radius: var(--radius);
    color: var(--ink); transition: border-color .25s, box-shadow .25s;
}
.textarea { min-height: 140px; resize: vertical; }
.input:focus, .textarea:focus, .select:focus { outline: 0; border-color: var(--gold-live); box-shadow: 0 0 0 3px rgba(236,164,36,.15); }
.on-ink .input, .on-ink .textarea, .on-ink .select { background: rgba(255,255,255,.04); border-color: var(--border-dark); color: var(--paper); }

/* Glassmorphic form card (contact) */
.form-glass {
    background: rgba(255,255,255,.06); backdrop-filter: blur(16px);
    border: 1px solid var(--border-dark); border-radius: var(--radius-lg); padding: var(--s-8);
}

/* ── Flash messages ──────────────────────────────────────────────────── */
.flash { padding: var(--s-3) var(--s-5); border-radius: var(--radius); margin-bottom: var(--s-4); font-size: .95rem; border: 1px solid; }
.flash-success { background: rgba(26,107,60,.1); border-color: var(--green); color: var(--green); }
.flash-error   { background: rgba(139,26,26,.08); border-color: var(--red); color: var(--red); }
.flash-info    { background: rgba(26,158,143,.1); border-color: var(--teal-lt); color: var(--teal); }

/* ── Stat counters ───────────────────────────────────────────────────── */
.stat { text-align: center; container-type: inline-size; min-width: 0; }
.stat-num {
    /* A NUMBER IS DATA, NOT DISPLAY TYPE. Oi runs ~0.8em per character, which
       is what made these overflow their columns this morning; Zain 800 needs
       ~0.5em for the same digits and reads cleanly at every size a stat
       renders at. Same exception fonts.css:19 already made for the logo. */
    font-family: var(--font-body); font-weight: 800;
    color: var(--gold-live); line-height: 1.05;
    /* Was clamp(2.6rem, 5vw, 4rem) — window-relative, so "1,500,000" in a
       4-up grid overflowed its column by 121px at 1024px (/community/press).
       cqi is a share of the STAT's OWN width, the box it must fit. 13cqi was
       a width concession to Oi; on Zain the same string fits at 17. */
    font-size: clamp(1.6rem, 17cqi, 4rem);
    white-space: nowrap;      /* a number shrinks; it never breaks mid-digit */
    max-width: 100%;
}
/* A money figure: the currency is a separate, smaller element so the number
   itself stays a 9-character string. Mirrors index.php .fund-counter. */
.stat-money { display: flex; align-items: baseline; justify-content: center;
              gap: .5rem; container-type: inline-size; min-width: 0; }
.stat-money .stat-cur { font-size: .85rem; color: var(--gold); letter-spacing: .2em; flex: 0 0 auto; }
.stat-money .stat-num { font-size: clamp(1.6rem, 15cqi, 4rem); }

.stat-label {
    font-family: var(--font-mono); font-size: .72rem; letter-spacing: .14em;
    text-transform: uppercase; color: var(--muted); margin-top: var(--s-3);
    overflow-wrap: anywhere; line-height: 1.5;
}
.on-ink .stat-label { color: var(--peach); }

/* ── Corvus widget ───────────────────────────────────────────────────── */
.corvus-btn {
    position: fixed; bottom: 24px; right: 24px; z-index: 200;
    width: 60px; height: 60px; border-radius: 50%; border: 0;
    background: var(--gold-live); color: var(--ink); box-shadow: var(--shadow-lg);
    display: grid; place-items: center; transition: transform .3s var(--ease);
}
.corvus-btn:hover { transform: scale(1.08); }
.corvus-btn svg { width: 28px; height: 28px; }
.corvus-panel {
    position: fixed; bottom: 96px; right: 24px; z-index: 200;
    width: min(380px, calc(100vw - 48px)); height: min(560px, calc(100vh - 140px));
    background: var(--ink2); border: 1px solid var(--border-dark); border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg); display: none; flex-direction: column; overflow: hidden;
}
.corvus-panel.open { display: flex; }
.corvus-head { padding: var(--s-5); border-bottom: 1px solid var(--border-dark); color: var(--paper); }
/* 1.3rem is squarely inside the size range fonts.css:19 flags Oi as unreadable
   at ("too heavy/wide for the small logo lockup text"). --font-wordmark is
   the face that comment introduced for exactly this case. Oi is also
   single-weight, so the font-weight:600 here never did anything. */
.corvus-head strong { font-family: var(--font-wordmark); font-size: 1.3rem; letter-spacing: .01em; }
.corvus-head span { display: block; font-size: .8rem; color: var(--peach); margin-top: 2px; }
.corvus-body { flex: 1; overflow-y: auto; padding: var(--s-5); display: flex; flex-direction: column; gap: var(--s-3); }
.corvus-msg { max-width: 85%; padding: .7rem 1rem; border-radius: 12px; font-size: .92rem; line-height: 1.5; }
.corvus-msg.user { align-self: flex-end; background: var(--gold-live); color: var(--ink); }
.corvus-msg.bot { align-self: flex-start; background: var(--ink3); color: var(--paper); }
.corvus-starters { display: flex; flex-wrap: wrap; gap: var(--s-2); padding: 0 var(--s-5) var(--s-3); }
.corvus-starter { font-size: .78rem; padding: .4rem .7rem; border: 1px solid var(--border-dark); border-radius: 20px; color: var(--peach); background: none; transition: all .2s; }
.corvus-starter:hover { border-color: var(--gold-live); color: var(--gold-live); }
.corvus-input { display: flex; gap: var(--s-2); padding: var(--s-4); border-top: 1px solid var(--border-dark); }
.corvus-input input { flex: 1; background: rgba(255,255,255,.05); border: 1px solid var(--border-dark); border-radius: 20px; padding: .6rem 1rem; color: var(--paper); font: inherit; font-size: .9rem; }
.corvus-input input:focus { outline: 0; border-color: var(--gold-live); }
.corvus-input button { background: var(--gold-live); color: var(--ink); border: 0; border-radius: 50%; width: 40px; height: 40px; display: grid; place-items: center; }
@media (max-width: 480px) { .corvus-panel { inset: 0; width: 100vw; height: 100vh; border-radius: 0; } }

/* ── Custom cursor — the surgeon's set ───────────────────────────────────
   Normal: golden scalpel (hotspot at the blade tip).
   Hover:  scalpel, engaged.   Click: scalpel, incising.
   Text:   golden femur I-beam.   Unavailable: crossed instrument.
   Loading: JS-drawn recurring ECG (cursor.js) — CSS cursors can't animate. */
html, body {
    cursor: url('/assets/images/cursors/normal.webp') 2 4, auto;
}
a, button, [role="button"], .btn, .abtn, label, select, summary,
input[type="submit"], input[type="checkbox"], input[type="radio"],
.corvus-btn, .corvus-starter, .nav-toggle, .disc-card, .card a {
    cursor: url('/assets/images/cursors/hover.webp') 2 4, pointer;
}
a:active, button:active, .btn:active, .abtn:active, [role="button"]:active {
    cursor: url('/assets/images/cursors/click.webp') 2 4, pointer;
}
input[type="text"], input[type="email"], input[type="password"],
input[type="search"], input[type="number"], input[type="tel"],
input[type="url"], input[type="date"], input[type="datetime-local"],
textarea, [contenteditable="true"], .input, .textarea {
    cursor: url('/assets/images/cursors/text.webp') 7 22, text;
}
[disabled], [aria-disabled="true"], .disabled,
button:disabled, input:disabled, textarea:disabled {
    cursor: url('/assets/images/cursors/unavailable.webp') 21 10, not-allowed;
}
/* While loading, the native cursor yields to the ECG overlay. */
body.cursor-loading, body.cursor-loading * { cursor: none !important; }
#medCursorLoad {
    position: fixed; top: 0; left: 0; width: 64px; height: 40px;
    pointer-events: none; z-index: 10000; display: none;
    filter: drop-shadow(0 0 6px rgba(236,164,36,.6));
}
body.cursor-loading #medCursorLoad { display: block; }
@media (hover: none) {
    html, body, a, button { cursor: auto; }
    #medCursorLoad { display: none !important; }
}

/* ── MedFX text splitting (fx.js) ────────────────────────────────────── */
/* Words are unbreakable units so char-level animation never wraps mid-word. */
.fx-word { display: inline-block; white-space: nowrap; }
.fx-char { display: inline-block; }

/* ── Reveal-on-scroll base ───────────────────────────────────────────── */
.reveal { opacity: 0; transform: translateY(24px); transition: opacity .8s var(--ease), transform .8s var(--ease); }
.reveal.in { opacity: 1; transform: none; }

/* ── Breadcrumb ──────────────────────────────────────────────────────── */
.breadcrumb { font-family: var(--font-mono); font-size: .72rem; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); padding-block: var(--s-5); }
.breadcrumb a { color: var(--muted); } .breadcrumb a:hover { color: var(--gold); }
.breadcrumb .sep { margin-inline: .5em; color: var(--gold-lt); }

/* ── Cookie consent banner ───────────────────────────────────────────── */
.consent-banner {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 300;
    background: var(--ink2); color: var(--peach);
    border-top: 1px solid var(--border-dark); box-shadow: 0 -10px 40px rgba(2,15,36,.42);
}
.consent-banner[hidden] { display: none; }
.consent-inner {
    max-width: var(--maxw); margin-inline: auto; padding: var(--s-5) var(--s-6);
    display: flex; align-items: center; justify-content: space-between; gap: var(--s-6); flex-wrap: wrap;
}
.consent-inner p { font-size: .92rem; max-width: 62ch; }
/* inline-block + padding takes the one link in this banner from 79x22 to the
   24px tap-target floor. It was the single most common target miss on the
   site — the banner is on every page, so it failed on all 49 routes at once. */
.consent-inner a { color: var(--gold-live); text-decoration: underline; text-underline-offset: 3px; display: inline-block; padding-block: .16rem; }
.consent-actions { display: flex; gap: var(--s-3); flex-wrap: wrap; }

/* ⚠ ON A SMALL PHONE THIS BANNER WAS 292px OF A 640px SCREEN — 46% of the
   viewport, permanently, on every page, until it is dismissed. It was found on
   the 404, where it covered "Take me back to safety" even after scrolling to
   it, but the banner is the cause and the 404 was only the page that made it
   visible: the same bar sits over the last 292px of every other page too.
   Two full-width stacked buttons and a six-line paragraph are what cost the
   height, so on narrow screens the buttons share a row and the copy tightens. */
@media (max-width: 560px) {
    .consent-inner { padding: var(--s-4) var(--s-4); gap: var(--s-3); }
    .consent-inner p { font-size: .82rem; line-height: 1.45; }
    .consent-actions { width: 100%; flex-wrap: nowrap; }
    .consent-actions .btn { flex: 1 1 0; min-width: 0; padding-inline: var(--s-3); font-size: .78rem; white-space: nowrap; }
}

/* ── Legal document prose (imported policies with tables) ────────────── */
.legal-prose { max-width: none; font-size: 1.02rem; }
.legal-prose table { width: 100%; border-collapse: collapse; font-size: .88rem; margin-block: 1.5em; display: block; overflow-x: auto; }
.legal-prose th { text-align: left; font-family: var(--font-mono); font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); border-bottom: 2px solid var(--border); padding: .6rem .7rem; }
.legal-prose td { border-bottom: 1px solid var(--border); padding: .6rem .7rem; vertical-align: top; }
.legal-prose h2 { font-size: 1.7rem; }
.legal-prose h3 { font-size: 1.3rem; }
.legal-prose hr { height: 1px; background: var(--border); border: 0; margin-block: 2em; }

/* ── Page hero (shared) ──────────────────────────────────────────────── */
.page-hero { padding-block: clamp(var(--s-10), 12vw, var(--s-16)); background: var(--ink); color: var(--paper); }
.page-hero h1 { max-width: 16ch; }
.page-hero .lead { margin-top: var(--s-5); max-width: 52ch; }


/* ═══════════════════════════════════════════════════════════════════════════
   RESPONSIVE TYPE INSIDE CARDS — added 2026-09-14.

   Every heading in this design system is set in Oi, a very wide display face,
   and sized in `vw`. That is correct for a full-bleed page heading and wrong
   for a heading inside a grid track: `vw` tracks the WINDOW, the track is a
   third or a quarter of it, and the mismatch is widest exactly where the grid
   is densest. `.card` is now a container (see .card above), so `cqi` — 1% of
   the CARD's inline size — sizes the heading from the box it has to fit.

   The vw fallback is kept as the outer clamp bounds so the desktop proportions
   the design was drawn at are unchanged; only the crowded cases move.
   ═══════════════════════════════════════════════════════════════════════════ */
.card h3 {
    font-size: clamp(1.15rem, 9.5cqi, 2.2rem);
    /* A single long word ("Physiotherapy", a university name) must break
       rather than widen the card. */
    overflow-wrap: break-word;
    hyphens: auto;
}
.card h4 {
    font-size: clamp(1rem, 7cqi, 1.35rem);
    overflow-wrap: break-word;
}
.card p { overflow-wrap: break-word; }

/* Four-up is too dense for a card with a heading between ~900px and ~1150px —
   global.css only drops it to two below 900. */
@media (min-width: 901px) and (max-width: 1150px) {
    .grid-4 { grid-template-columns: repeat(2, 1fr); }
}

/* Long unbroken strings anywhere: URLs in legal text, emails in contact
   panels, DOIs in the journal. These are the classic single-element causes of
   a page that scrolls sideways on a phone. */
.prose a, .prose code, .footer-bottom a { overflow-wrap: anywhere; }

/* Tables are the one thing allowed to be wider than the screen, and only
   inside their own scroller. */
.prose table, table.data { display: block; max-width: 100%; overflow-x: auto; }

/* ── Footer social row ───────────────────────────────────────────────────
   Driven by the `social_links` table (/admin/social). The whole row is absent
   until at least one account is added, so an empty list costs nothing. */
/* ⚠ ONE ROW BETWEEN TWO RULES (Mo, 2026-09-15, with a screenshot).
   The social icons and the store badges used to be two stacked blocks, each
   with its own vertical padding, and `.footer-bottom` then added
   `margin-top: var(--s-10)` underneath — which is the large empty band he
   marked. They are now the two ends of a single row: the rule above sits
   directly on top of the icons, the rule below sits directly under them, and
   there is no stack left to leave a gap. */
.footer-meta-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--s-5);
    padding-block: var(--s-4);
    border-top: 1px solid var(--border-dark);
}
.footer-meta-row .app-links { margin-block: 0; }
.footer-meta-row .app-links-download { padding-top: 0; border-top: 0; }
.footer-meta-row .app-badge img { height: 48px; }

.footer-social {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-3);
}
/* A row of circular glyph buttons. The <a> is sized, not the svg, so the
   tap target stays 44px on a phone however small the glyph is drawn. */
.footer-social a.social-ico {
    display: grid;
    place-items: center;
    inline-size: 44px;
    block-size: 44px;
    border-radius: 50%;
    color: var(--gold-lt);
    border: 1px solid var(--border-dark);
    background: rgba(255, 255, 255, .02);
    transition: color .2s, border-color .2s, background .2s, transform .2s;
}
.footer-social a.social-ico svg {
    inline-size: 18px;
    block-size: 18px;
    fill: currentColor;
    display: block;
}
.footer-social a.social-ico:hover,
.footer-social a.social-ico:focus-visible {
    color: var(--ink);
    background: var(--gold-live);
    border-color: var(--gold-live);
    transform: translateY(-2px);
}
.footer-social a.social-ico:focus-visible { outline: 2px solid var(--gold-live); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
    .footer-social a.social-ico { transition: none; }
    .footer-social a.social-ico:hover { transform: none; }
}

/* ── Content protection (Mo 2026-09-15) ───────────────────────────────────
   The CSS half of assets/js/protect.js: it stops the drag-select before any
   event fires, and it keeps working if the script is blocked. ⛔ The exemption
   list below is not optional — a visitor who cannot select the email they
   mistyped, or paste a password from a manager, cannot sign up. Public pages
   carry the login, signup, contact, search and Corvus fields.
   ⚠ `.allow-select` opts an element back in for anything a visitor is MEANT to
   take away: an access code, an address, a bank reference. */
/* ⛔ `:not(.admin-ui)` IS LOAD-BEARING. protect.js is wired into the PUBLIC
   footer only, so the admin never gets the JS half — but this stylesheet is
   shared, so without the exclusion the admin inherited `user-select: none` and
   Mo could not select a value out of any table, log line or read-only field on
   his own console. Measured on /admin before the class existed.
   The class is stamped in templates/_admin-header.php. */
body:not(.admin-ui) {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
}
input, textarea, select,
[contenteditable], [contenteditable="true"],
.allow-select, .allow-select * {
    -webkit-user-select: text !important;
    user-select: text !important;
    -webkit-touch-callout: default;
}
img, picture, svg { -webkit-user-drag: none; user-drag: none; }

/* ── Tap-target floor (WCAG 2.2 §2.5.8, measured 2026-09-15) ──────────────
   tools/check-responsive.mjs found these at EVERY width, not just on phones,
   so they are not a responsive bug — they were always this small:
     .footer-col a   75x22   .breadcrumb a   40x15   .footer-bottom a   ~x15
   The minimum is 24x24. This adds vertical padding only: the type size, the
   colour and the line rhythm are untouched, so nothing about the design
   changes — the hit area simply stops being smaller than a fingertip. */
.footer-col a { padding-block: .42rem; }
.breadcrumb a, .breadcrumb span { display: inline-block; padding-block: .3rem; }
.footer-bottom a { display: inline-block; padding-block: .3rem; }

/* ── Store badges: app downloads + "Listen on" (Mo, 2026-09-15) ────────────
   Mo asked for *"rise up on hover animation and glowing edge"* on the podcast
   row; the footer row uses the same treatment so the two look like one system.

   ⚠ The badges are sized by HEIGHT, never width. The five PNGs are not the same
     aspect ratio (Spotify's lockup is wider than the App Store's), so a fixed
     width makes them different physical sizes and the row looks broken. Height
     + `width:auto` is what makes a mixed set of store badges sit level. */
.app-links { margin-block: var(--s-6); }
.app-links-label {
    display: block; font-family: var(--font-mono); font-size: .72rem;
    letter-spacing: .16em; text-transform: uppercase; color: var(--gold);
    margin-bottom: var(--s-3);
}
.app-links-row { display: flex; flex-wrap: wrap; gap: var(--s-4); align-items: center; }

.app-badge {
    display: inline-flex;
    border-radius: 10px;
    /* The rise and the glow are separate properties on purpose: `transform` is
       composited and `box-shadow` is not, so animating them together in one
       shorthand would drop the rise onto the paint thread with the glow. */
    transition: transform .28s var(--ease), box-shadow .28s var(--ease), filter .28s var(--ease);
    will-change: transform;
}
.app-badge img {
    height: 56px; width: auto; display: block; border-radius: 10px;
}
/* ⚠ NO RIM ON HOVER (Mo, 2026-09-15): *"remove the highlight stroke effect on
   hover just keep the rise up."* The gold `0 0 0 1px` ring traced the badge's
   bounding BOX, not its artwork — and every store badge already carries its
   own rounded black plate, so the ring read as a second, mismatched border
   floating a pixel outside the real one. The lift and a soft drop shadow do
   the whole job. */
.app-badge:hover,
.app-badge:focus-visible {
    transform: translateY(-6px);
    box-shadow: 0 12px 26px rgba(2, 15, 36, .55);
}
.app-badge:focus-visible { outline: 2px solid var(--gold-live); outline-offset: 3px; }

/* The podcast row is a page section rather than a footer strip, so it is
   centred and given room; the footer keeps its left-aligned column rhythm. */
.app-links-listen { text-align: center; margin-block: var(--s-8); }
.app-links-listen .app-links-row { justify-content: center; }

/* The footer row sits between the link columns and the social icons. */
.app-links-download { padding-top: var(--s-6); border-top: 1px solid var(--border-dark); }

@media (max-width: 560px) {
    .app-badge img { height: 46px; }
    .app-links-row { gap: var(--s-3); justify-content: center; }
    .app-links-download { text-align: center; }
}

/* A badge that leaps on hover is precisely the motion this preference exists to
   stop; the glow stays, because it is the part that says "this is a link". */
@media (prefers-reduced-motion: reduce) {
    .app-badge { transition: box-shadow .28s var(--ease); }
    .app-badge:hover, .app-badge:focus-visible { transform: none; }
}

/* ── The cookie-policy animation in the consent banner (Mo, 2026-09-15) ────
   *"size it properly in proportion to text next to it and centralize it to its
   horizontal access."*

   ⚠ SIZED FROM THE TEXT, NOT IN PIXELS. The artwork is square (400x400,
     measured). A fixed px size looks right at one breakpoint and wrong at every
     other, because the paragraph beside it wraps from two lines on a desktop to
     six on a small phone. Tying the square to the row's own content height via
     `align-self: stretch` + `aspect-ratio: 1` makes it track the text at every
     width, with a clamp so it can never dominate a narrow screen or shrink to a
     dot on a wide one.
   ⚠ `align-items: center` on `.consent-inner` (already set) is the other half:
     it centres the square on the row's horizontal axis. */
.consent-lottie {
    flex: 0 0 auto;
    align-self: center;
    /* ⚠ ENLARGED 2026-09-15. Mo: *"enlarge the cookies lottie json animation."*
       Was clamp(46px, 7vw, 74px) — measured 74px against a 123px-tall banner at
       1440, so the square read as an icon rather than the illustration it is.
       ⛔ The ceiling is what keeps the banner one row: at 1440 the text block is
          two lines (~54px) and the banner is 123px tall, so a square much past
          ~104px starts driving the banner's height instead of fitting inside it.
          Measured after the change rather than assumed. */
    width: clamp(56px, 8.4vw, 104px);
    aspect-ratio: 1;
    /* Until lottie-web resolves, this is an empty div — without a reserved box
       the banner would reflow the moment the animation appeared. */
    min-height: clamp(56px, 8.4vw, 104px);
    pointer-events: none;
}
.consent-lottie svg { display: block; width: 100% !important; height: 100% !important; }

/* The paragraph must keep the row's width once a square is beside it, or the
   text column collapses and the banner grows a line taller. */
.consent-inner p { flex: 1 1 320px; min-width: 0; }

@media (max-width: 560px) {
    /* On a phone the banner is already nearly half the screen (it was measured
       at 292px of 640 before it was tightened). The square sits with the text
       rather than above it, small, so it costs a few pixels and not a row. */
    /* ⚠ Grown with the rest, but far less: this banner is already a quarter
       of a 390x844 screen (measured 209px of 844), and the square shares the
       row with three lines of text. 40 -> 54 is visible without adding a row. */
    .consent-lottie { width: 54px; min-height: 54px; }
}
