/* ══════════════════════════════════════════════════════════════════════════
   ANUPET ACADEMY — anupet.academy
   Light-first, English-first. Ink on paper, with full-bleed dark reserved for
   the photographic sections. Inter Tight carries the headings and Instrument
   Serif is the one accent voice; Azonix from the brand kit is kept for the
   small lockup only, where its weight reads as a mark rather than as shouting.

   Palette from the identity: #00428d · #003366 · #0f52ba · #ff6b35 · #dfdfdf
   Layout uses CSS logical properties, so one ruleset serves RTL and LTR.
   ══════════════════════════════════════════════════════════════════════════ */

@font-face{
  font-family:'beIN';
  src:url('../fonts/bein.woff2') format('woff2');
  font-weight:400;font-style:normal;font-display:swap;
}
@font-face{
  font-family:'Azonix';
  src:url('../fonts/azonix.woff2') format('woff2');
  font-weight:400;font-style:normal;font-display:swap;
}

:root{
  color-scheme: light;

  /* brand */
  --navy:      #00428d;
  --deep:      #003366;
  --sapphire:  #0f52ba;
  --azure:     #1466e0;
  --sky:       #58b0f5;
  --orange:    #ff6b35;
  --orange-dk: #cc4a12;

  /* paper */
  --bg:      #f1f3f7;   --bg-rgb: 241,243,247;
  --bg2:     #ffffff;
  --bg3:     #e7eaf1;
  --ink:     #081426;   --ink-rgb: 8,20,38;
  --mut:     #566880;   --mut-rgb: 86,104,128;
  --line:    rgba(8,20,38,.10);
  --line2:   rgba(8,20,38,.18);
  --shadow:  0 16px 40px rgba(8,20,38,.08);
  --shadow2: 0 34px 90px rgba(8,20,38,.15);
  /* (public-ux · forms) The form focus colour, as a token rather than a
     literal --navy: #00428d is dark blue on a #04070e field in dark mode,
     which is a focus ring nobody can see. One name, two values. */
  --focus:      #00428d;
  --focus-ring: rgba(0,66,141,.15);

  --dark:    #04070e;

  /* type */
  --ff-x: 'Azonix','Inter Tight',sans-serif;                /* brand lockup only */
  --ff-s: 'Instrument Serif',Georgia,serif;                 /* accent voice      */
  --ff-a: 'IBM Plex Sans Arabic',sans-serif;                /* Arabic headings   */
  --ff-d: 'Inter Tight','IBM Plex Sans Arabic',sans-serif;  /* headings          */
  --ff-b: 'Inter','IBM Plex Sans Arabic',sans-serif;        /* body              */
  --ff-m: 'Inter Tight',ui-monospace,monospace;             /* labels & numbers  */

  --ease:  cubic-bezier(.16,1,.3,1);
  --ease2: cubic-bezier(.4,0,.2,1);

  --pad:  clamp(18px,3.6vw,60px);
  --maxw: 1760px;
}

/* dark surfaces — scoped token flip rather than a second stylesheet */
.on-dark{
  --bg:#04070e; --bg-rgb:4,7,14; --bg2:#080f1c; --bg3:#0d1828;
  --ink:#eaf2fc; --ink-rgb:234,242,252; --mut:#93aac9;
  --line:rgba(234,242,252,.11); --line2:rgba(234,242,252,.2);
  --azure:#58b0f5;
  --focus:#58b0f5; --focus-ring:rgba(88,176,245,.22);
  --shadow:0 16px 40px rgba(0,0,0,.5); --shadow2:0 34px 90px rgba(0,0,0,.62);
  color-scheme:dark; background:var(--bg); color:var(--ink);
}
html[data-theme="dark"]{
  color-scheme:dark;
  --bg:#04070e; --bg-rgb:4,7,14; --bg2:#080f1c; --bg3:#0d1828;
  --ink:#eaf2fc; --ink-rgb:234,242,252; --mut:#93aac9;
  --line:rgba(234,242,252,.11); --line2:rgba(234,242,252,.2);
  --azure:#58b0f5;
  --focus:#58b0f5; --focus-ring:rgba(88,176,245,.22);
  --shadow:0 16px 40px rgba(0,0,0,.5); --shadow2:0 34px 90px rgba(0,0,0,.62);
}

*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
/* `scrollbar-gutter` pairs with the scroll lock further down: that rule puts
   `overflow:hidden` on HTML, which on a classic-scrollbar platform (Windows and
   Linux Chrome/Firefox) takes the ~15px scrollbar away for as long as the drawer
   is open, and the page behind it jumps sideways by that much and back again on
   close. Reserving the gutter in both states costs nothing where scrollbars are
   already overlays, which is every phone. */
html{overflow-x:clip;-webkit-text-size-adjust:100%;scroll-behavior:auto;scrollbar-gutter:stable}
html.lenis,html.lenis body{height:auto}
.lenis.lenis-smooth{scroll-behavior:auto!important}

body{
  background:var(--bg); color:var(--ink);
  font-family:var(--ff-b); font-size:16px; line-height:1.72;
  overflow-x:clip; -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
  transition:background .5s var(--ease2),color .5s var(--ease2);
}
body.is-locked{overflow:hidden}
/* (owner · phone) The scroll lock above cost the visitor the way out of the
   menu, and on a phone that is the whole menu. `html` is not `overflow:visible`
   here — it is `overflow-x:clip` — so the body's overflow does NOT propagate to
   the viewport the way it does on a default document. It makes BODY a scroll
   container of its own, at scroll offset 0, and `.nav`'s `position:sticky`
   then sticks to that instead of to the viewport. Measured on a 375x812 phone
   scrolled to y 1400 with the drawer open: the bar sat at y -1366.4, i.e. the
   burger — which is the close X, the only control that dismisses the drawer on
   a touch screen — was a thousand pixels above the top of the screen. The
   screenshot is a full black sheet of links with no header on it at all.
   The lock belongs one level up, on the element that actually owns the
   viewport's scrollport. With it there the body goes back to being ordinary
   flow (`clip`, so it still cannot scroll on its own but is not a scroll
   container either) and the sticky bar has the viewport to stick to again:
   measured after, nav at y 0, `elementFromPoint(35,33)` returns the burger,
   no content shift behind the drawer, and a 400px wheel still moves scrollY
   not at all.
   The `body.is-locked` rule above is deliberately left as it was rather than
   replaced: where `:has()` is not supported both of these drop and the lock is
   exactly the one that shipped, which is the failure this replaces — not a
   page that scrolls away behind an open menu. */
html:has(body.is-locked){overflow:hidden}
html:has(body.is-locked) body.is-locked{overflow:clip}
img,video{max-width:100%;display:block}
svg{display:block}
.ico{width:1em;height:1em;flex:0 0 auto}
a{color:inherit;text-decoration:none}
button,input,textarea,select{font:inherit;color:inherit}
::selection{background:var(--navy);color:#fff}

:where(h1,h2,h3,h4){font-family:var(--ff-d);font-weight:600;line-height:1.1;letter-spacing:-.022em}
[lang="ar"] :where(h1,h2,h3,h4){font-family:var(--ff-a);font-weight:600;line-height:1.4;letter-spacing:-.005em}

/* Azonix has caps only — it is used for display strings, never for body copy */
.azo{font-family:var(--ff-x);text-transform:uppercase;letter-spacing:.02em;font-weight:400}
.ser{font-family:var(--ff-s);font-style:italic;font-weight:400;letter-spacing:-.012em}

/* ═════════════════════════════════════════════════════════ primitives ══ */
.wrap{max-width:var(--maxw);margin-inline:auto;padding-inline:var(--pad)}
.wrap--tight{max-width:1300px}
.sect{position:relative;padding-block:clamp(66px,6.2vw,104px)}
.sect--band{background:var(--bg2);border-block:1px solid var(--line)}

/* Off-screen but readable by assistive tech. The identity band needs it: its
   artwork carries baked English lettering, so the band's name has to be a real
   bilingual element the language toggle can swap, not an aria-label attribute
   (the toggle rewrites innerHTML and never touches attributes). */
.vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0}

.eyebrow{font-family:var(--ff-m);font-weight:500;font-size:11.5px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--azure);display:inline-flex;align-items:center;gap:12px}
.eyebrow::before{content:'';width:34px;height:2px;background:currentColor;opacity:.6;border-radius:2px}
[lang="ar"] .eyebrow{font-family:var(--ff-b);font-weight:700;letter-spacing:.04em;font-size:13.5px}

.h-hero{font-size:clamp(2.2rem,5vw,4.6rem)}
.h-xl  {font-size:clamp(1.85rem,3.4vw,3.2rem)}
.h-lg  {font-size:clamp(1.65rem,2.7vw,2.5rem)}
.h-md, .page__cms h2{font-size:clamp(1.2rem,1.6vw,1.6rem)}
.lead{color:var(--mut);font-size:clamp(.99rem,1vw,1.1rem);line-height:1.8}
.muted{color:var(--mut)}

/* a heading that runs the full width with its note pushed to the far edge */
.spread{display:flex;align-items:flex-end;justify-content:space-between;
  gap:clamp(22px,4vw,100px);flex-wrap:wrap}
.spread__side{max-width:54ch;color:var(--mut);font-size:1rem;line-height:1.8}

/* (D-054) The orphan `.shiny` colour utility and its unused `@keyframes shine`
   that used to sit here were deleted: neither had a single user anywhere in
   the repo, and the name collided with the headline sweep this round adds.
   One `.shiny`, one `@keyframes shine` — both in the hero block below. */

.glass{position:relative;border-radius:22px;background:rgba(255,255,255,.66);
  backdrop-filter:blur(16px) saturate(155%);-webkit-backdrop-filter:blur(16px) saturate(155%);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.75),var(--shadow)}
.on-dark .glass{background:rgba(255,255,255,.05);
  box-shadow:inset 0 1px 1px rgba(255,255,255,.14),0 20px 54px rgba(0,0,0,.5)}

/* ═══════════════════════════════════════════════════════════ buttons ═══ */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;border:0;cursor:pointer;
  padding:14px 26px;border-radius:999px;font-weight:600;font-size:.96rem;position:relative;
  overflow:hidden;
  transition:transform .4s var(--ease),scale .4s var(--ease),background .3s,box-shadow .4s,color .3s}
.btn .ico{width:18px;height:18px;transition:transform .4s var(--ease)}
[dir="rtl"] .btn .ico{transform:scaleX(-1)}
.btn:hover .ico{transform:translateX(5px)}
[dir="rtl"] .btn:hover .ico{transform:scaleX(-1) translateX(5px)}
/* D-036: buttons never move on hover — press feedback is the only motion
   left, a plain scale that reverses the instant the pointer releases.
   (r2 review) The independent `scale` property, not transform: GSAP leaves an
   inline `transform` on any button it has tweened (the hero CTAs), and an
   inline declaration outranks this rule — with `transform:scale()` here the
   press was silently dead on exactly the two most-clicked buttons. `scale`
   composes with an inline transform instead of being overwritten by it. */
.btn:active{scale:.97}
.btn--pri{background:var(--navy);color:#fff;box-shadow:0 14px 34px rgba(0,66,141,.3)}
.btn--pri:hover{background:var(--sapphire);
  box-shadow:0 18px 44px rgba(15,82,186,.42),inset 0 0 0 1px rgba(255,255,255,.18)}
.btn--acc{background:var(--orange);color:#2a0c02;box-shadow:0 14px 34px rgba(255,107,53,.36)}
.btn--acc:hover{filter:brightness(1.07);
  box-shadow:0 14px 34px rgba(255,107,53,.36),inset 0 0 0 1px rgba(255,255,255,.28)}
.btn--ghost{background:transparent;color:var(--ink);box-shadow:inset 0 0 0 1.6px var(--line2)}
.btn--ghost:hover{box-shadow:inset 0 0 0 1.6px var(--navy);color:var(--navy)}
.on-dark .btn--ghost:hover{color:#fff;box-shadow:inset 0 0 0 1.6px var(--sky)}
.btn--sm{padding:11px 21px;font-size:.92rem}
.btn--block{width:100%}

/* ═════════════════════════════════════════════════════ page furniture ══ */
.grain{position:fixed;inset:0;z-index:200;pointer-events:none;opacity:.035;mix-blend-mode:multiply;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}
html[data-theme="dark"] .grain{mix-blend-mode:overlay;opacity:.05}
.progress{position:fixed;inset-block-start:0;inset-inline-start:0;height:3px;width:0;z-index:220;
  background:linear-gradient(90deg,var(--navy),var(--sapphire),var(--orange))}

/* ══════════════════════════════════════════════ anubi · scoped tokens ═══ */
/* (anubi) The sanctum's palette. These are declared on `.hero` and `#ldr` and
   NOWHERE ELSE — not on :root — because they are a scene's colours, not the
   site's. The academy's own tokens (--navy, --sky, --bg, the two themes)
   describe a bright product page and are read by three hundred other rules;
   dropping a gold/obsidian set into :root would have repainted every one of
   them. Scoping instead means the theme toggle keeps working everywhere and
   the two dark surfaces below simply do not participate — they are obsidian
   in both themes on purpose, the way the cinema sections already are.
   Values are the lab's, unchanged. */
.hero,
#ldr{
  --au-ink:#06050a;
  --au-stone-800:#120f18;
  --au-stone-700:#1e1929;
  --au-line:#2e2740;
  --au-line-soft:#221d30;
  --au-gold-300:#f4dfa0;
  --au-gold-400:#e8c96a;
  --au-gold-500:#c9a227;
  --au-gold-600:#8d6f18;
  --au-papyrus:#efe4cd;
  --au-faience:#3ab5aa;
  /* Amiri is loaded on the home page only (see the css2 <link> there); the
     fallbacks are what a standalone page or a blocked font request lands on. */
  --au-ff-dis:"Amiri","Instrument Serif",Georgia,"Times New Roman",serif;
  --au-out:cubic-bezier(.22,1,.36,1);
  --au-io:cubic-bezier(.65,0,.35,1);
}

/* ═══════════════════════════════════════════════════════════ preloader ═ */
/* (anubi) The mummy transformation, on the ground the hero stands in. The
   canvas is the picture; the brand, the bar, the hint and the percentage sit
   under it in one centred column. site.js still owns the LIFT — it writes
   #ldrBar's width and #ldrPc's number, and it removes this element — so the
   bar stays width-driven rather than adopting the lab's scaleX. */
.ldr{position:fixed;inset:0;z-index:400;background:var(--au-ink);
  display:grid;place-items:center;align-content:center;gap:16px}
/* The gold pool the statue is lit from. A pseudo-element rather than a
   gradient on .ldr itself, because the ground must stay flat obsidian behind
   the canvas's own drop shadow. */
.ldr::before{content:'';position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(60% 50% at 50% 40%,rgba(232,201,106,.14),transparent 70%)}
/* No drop-shadow. It was here to lift the figure off the flat obsidian, and it
   worked while the only thing on the canvas was a figure. The repaired levitate
   take puts golden ribbons out to both edges of a 1092-wide frame, and a
   drop-shadow is computed from the element's alpha BEFORE the mask trims it — so
   the shadow of ribbons that reach the frame edge is a soft dark band down the
   canvas's own boundary, offset 30px and blurred 40. That is the square around
   the preloader. The mask's edge fade is what softens the figure now, and the
   gold pool from .ldr::before is what separates it from the ground. */
.ldr__canvas{position:relative;-webkit-mask-image:linear-gradient(to right,transparent,#000 12%,#000 88%,transparent),linear-gradient(to bottom,transparent,#000 8%,#000);-webkit-mask-composite:source-in;mask-image:linear-gradient(to right,transparent,#000 12%,#000 88%,transparent),linear-gradient(to bottom,transparent,#000 8%,#000);mask-composite:intersect}
.ldr__brand{position:relative;font-family:var(--au-ff-dis);font-size:30px;
  line-height:1.2;color:var(--au-papyrus)}
/* The bar is now a TRACK plus a fill, because site.js writes a percentage
   width onto #ldrBar itself: with the gold on the same element the visitor
   would see a 2px hairline growing out of nothing instead of a groove
   filling, and a percentage needs a sized parent to resolve against at all. */
.ldr__track{position:relative;width:min(240px,60vw);height:2px;border-radius:2px;
  background:var(--au-line);overflow:hidden}
.ldr__bar{position:absolute;inset-block-start:0;inset-inline-start:0;height:100%;width:0;
  background:linear-gradient(90deg,var(--au-gold-500),var(--au-gold-300))}
.ldr__hint{position:relative;font-size:12px;color:rgba(239,228,205,.5)}
/* Kept, and muted: the number is a diagnostic the page has always shown, but
   it is no longer the thing being watched. */
.ldr__pc{position:absolute;inset-block-end:26px;inset-inline-end:var(--pad);
  font-family:var(--ff-m);font-size:12px;letter-spacing:.2em;
  color:rgba(239,228,205,.28)}

/* ═══════════════════════════════════════════════════════════════ nav ═══ */
/* (D-056) The strip is obsidian, not the page surface. It is not sticky, so
   the ONLY thing it is ever seen against is the top of the hero — and a white
   band above a near-black film read as a torn-off edge. Making it dark in both
   themes turns it into the first line of one continuous header block with the
   bar below it. Literal colours for the same reason the cinema sections use
   them: this strip's backdrop is fixed dark whatever the theme says.
   (The standalone pages use `.page__top`, not this, so they are untouched.) */
.topbar{position:relative;z-index:120;border-block-end:1px solid rgba(88,176,245,.16);
  background:#04070e;font-size:12px;color:rgba(234,242,252,.62)}
.topbar__in{display:flex;align-items:center;gap:20px;flex-wrap:wrap;
  max-width:var(--maxw);margin-inline:auto;padding:6px var(--pad)}
.topbar a{display:inline-flex;align-items:center;gap:7px;transition:color .25s}
.topbar a:hover{color:var(--sky)}
.topbar .ico{width:14px;height:14px;opacity:.75;color:var(--sky)}
.topbar__hrs{display:inline-flex;align-items:center;gap:8px}
.topbar__sp{margin-inline-start:auto}
.topbar__soc{display:flex;gap:15px}

/* ── (D-054) slim obsidian glass bar ────────────────────────────────────────
   The floating capsule is gone. The bar is now full-bleed and has exactly two
   states, and EVERY colour inside it is driven from the six tokens below
   rather than from the page palette — that is what lets one set of rules serve
   a bar sitting on the near-black hero AND the same bar sitting on the page's
   own surface after it scrolls past. State A (default) is translucent obsidian
   with an electric-blue hairline, floating over the hero. State B (`is-stuck`,
   set by chrome() once the hero has scrolled away) is the solid theme surface
   with a shadow. Only the tokens change between them, so the transition is one
   interpolation and there is no second copy of the layout. */
.nav{
  /* (D-056) The obsidian state was measured washed out: idle links at .66
     alpha and a .16-alpha accent chip read as grey text and a ghost blob
     against the film. The glass is darker now (.62, so the bar carries its own
     ground rather than borrowing whatever frame is behind it), idle links sit
     at #cfe0f4 — a real colour, not a faded white — and the active state is
     white type on a solid-enough accent chip. Measured over the hero's
     brightest frame, not just its darkest: see the report's contrast table. */
  --nav-bg:rgba(4,7,14,.72);
  --nav-ink:#ffffff;
  --nav-mut:#cfe0f4;
  --nav-hair:rgba(88,176,245,.45);
  --nav-hover:rgba(255,255,255,.14);
  --nav-accent:var(--sky);
  --nav-accent-bg:rgba(88,176,245,.34);
  --nav-active-ink:#ffffff;
  --nav-shadow:none;
  position:sticky;inset-block-start:0;z-index:130;
  background:var(--nav-bg);
  backdrop-filter:blur(18px) saturate(165%);
  -webkit-backdrop-filter:blur(18px) saturate(165%);
  border-block-end:1px solid var(--nav-hair);
  box-shadow:var(--nav-shadow);
  transition:background .4s var(--ease),border-color .4s var(--ease),
             box-shadow .4s var(--ease)}
.nav.is-stuck{
  --nav-bg:var(--bg2);
  --nav-ink:var(--ink);
  --nav-mut:var(--mut);
  --nav-hair:var(--line);
  --nav-hover:rgba(8,20,38,.06);
  --nav-accent:var(--navy);
  --nav-accent-bg:rgba(0,66,141,.10);
  --nav-active-ink:var(--navy);
  --nav-shadow:0 10px 34px rgba(8,20,38,.10)}
html[data-theme="dark"] .nav.is-stuck{
  --nav-hover:rgba(255,255,255,.08);
  --nav-accent:var(--sky);
  --nav-accent-bg:rgba(88,176,245,.14);
  --nav-active-ink:var(--sky);
  --nav-shadow:0 10px 34px rgba(0,0,0,.5)}
.nav__in{display:flex;align-items:center;gap:clamp(8px,1vw,18px);
  max-width:var(--maxw);margin-inline:auto;padding:11px var(--pad);
  color:var(--nav-ink);transition:color .4s var(--ease)}

.brand{display:flex;align-items:center;gap:9px;flex-shrink:0}
.brand__mark{width:30px}
.brand__mark img{width:100%;height:auto}
.brand__txt{line-height:1.05}
.brand__name{font-family:var(--ff-x);font-weight:400;font-size:13px;letter-spacing:.09em;display:block}
/* (responsive sweep R8) 6.5px is below any readable floor — the sub-wordmark
   was a grey smudge rather than a word. Raised to 8.5px with the tracking
   pulled back from .34em to .22em so the lockup keeps its proportions: the
   sub stays narrower than the 13px .brand__name above it, which is the
   relationship that makes it read as a subtitle. */
.brand__sub{font-family:var(--ff-x);font-size:8.5px;letter-spacing:.22em;color:var(--azure);display:block}
/* inside the bar the sub-wordmark follows the bar's accent, not the page's:
   --azure is a fixed brand blue and disappears against the obsidian state */
.nav .brand__sub{color:var(--nav-accent);transition:color .4s var(--ease)}

/* (D-054) The pill mechanic survives the new bar — it was the one part of the
   old capsule worth keeping — but every colour in it now comes from the bar's
   own tokens, so the same rule reads correctly over the hero and over the
   page surface. */
.menu{position:relative;display:flex;align-items:center;gap:2px;margin-inline:auto}
.menu a{position:relative;padding:8px 13px;border-radius:999px;font-size:13.5px;font-weight:500;
  color:var(--nav-mut);transition:color .25s,background .25s;white-space:nowrap}
.menu a:hover{color:var(--nav-ink);background:var(--nav-hover)}
/* (D-056) The active state needs to be unmistakable on both grounds: over the
   hero a tinted chip alone read as a smudge, so the type goes to full white
   and the chip gains a hairline of the accent itself. On the scrolled surface
   the same two tokens resolve to navy-on-navy-tint, which is the treatment the
   page already used. */
.menu a.is-active{color:var(--nav-active-ink);background:var(--nav-accent-bg);
  box-shadow:inset 0 0 0 1px var(--nav-accent);font-weight:600}
.menu__has{display:flex;align-items:center;gap:7px}
.menu__has .ico{width:13px;height:13px;transition:transform .3s}
.nav__grp{position:static}
.nav__grp:hover .menu__has .ico{transform:rotate(180deg)}

/* (D-039) The dropdown opens over the dark photographic hero, where the old
   .72-alpha glass pulled the hero's near-black through and read as a murky
   grey slab with grey-on-grey text. It is now a near-solid surface with only
   a light blur behind it, so the panel is its own object on the page and the
   track titles sit at full ink contrast in both themes. */
/* (D-054) Reconciled with the new bar: the panel now hangs 8px off a
   full-bleed edge rather than 14px off a floating capsule, and its radius
   drops to 18px so it belongs to the same, squarer family. The blue top
   hairline is the bar's own hairline continuing down into the panel, which is
   what makes the dropdown read as descending FROM the bar rather than
   hovering near it. It keeps the page's surface colour in both states: it is
   an object on the page, and a panel that changed palette with the bar would
   be a second design to maintain. */
.mega{position:absolute;inset-block-start:calc(100% + 8px);inset-inline-start:50%;translate:-50% 12px;
  width:min(92vw,760px);padding:14px;border-radius:18px;z-index:140;
  background:rgba(255,255,255,.96);
  backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);
  /* the shorthand first, THEN the block-start override — reversing the two
     would let `border` reset the blue hairline back to --line2 */
  border:1px solid var(--line2);
  border-block-start:2px solid var(--sky);
  box-shadow:var(--shadow2);
  display:grid;grid-template-columns:repeat(2,1fr);gap:6px;
  opacity:0;visibility:hidden;transition:all .35s var(--ease)}
[dir="rtl"] .mega{translate:50% 12px}
.nav__grp:hover .mega,.mega:hover,.nav__grp:focus-within .mega{opacity:1;visibility:visible;translate:-50% 0}
[dir="rtl"] .nav__grp:hover .mega,[dir="rtl"] .mega:hover,[dir="rtl"] .nav__grp:focus-within .mega{translate:50% 0}
/* (r3) BOTH boxes need `min-width:0` — the item and its text column.
   `min-width:auto` is the default for a grid item AND for a flex item, so the
   long track name (04 · ICDL — International Computer Driving Licence) pushed
   the row past its grid column and then past the panel's edge instead of
   wrapping. Measured: 37px outside the panel before, 0 after. Wrapping onto a
   second line is the deliberate choice here — truncating a course name with
   an ellipsis loses information. */
.mega__i{position:relative;display:flex;align-items:flex-start;gap:13px;padding:13px;
  border-radius:16px;transition:background .25s;min-width:0}
.mega__i:hover{background:color-mix(in srgb,var(--accent) 8%,transparent)}
.mega__c{min-width:0}
.mega__i::before{content:'';position:absolute;inset-block:10px;inset-inline-start:0;width:2px;
  border-radius:2px;background:var(--accent);transform:scaleY(0);transform-origin:50% 0;
  transition:transform .35s var(--ease)}
.mega__i:hover::before{transform:scaleY(1)}
html[data-theme="dark"] .mega{background:rgba(10,18,34,.96);border-color:rgba(255,255,255,.14)}
/* the track number becomes a tinted chip so each row is identifiable at a
   glance without the panel needing a second colour anywhere else */
.mega__n{flex:0 0 auto;width:34px;height:34px;border-radius:50%;display:grid;place-items:center;
  font-family:var(--ff-m);font-size:13px;font-weight:600;padding:0;
  background:color-mix(in srgb,var(--accent,var(--azure)) 12%,transparent);
  /* the numeral is 13px type on a 12%-tinted chip — the raw orange accent
     measured 2.37:1 there, so it takes the lift like every other accent TEXT */
  color:var(--accent-lift,var(--azure))}
/* (D-058) Title-length-proof. `.mega__i` is an <a> inside `.menu`, so it
   inherited `white-space:nowrap` from the nav-link rule — which is right for a
   one-word menu item and wrong for a course title. The ICDL row measured 478px
   of text laid out on ONE line inside a 289px cell with overflow visible: the
   title spilled straight across the neighbouring column and dragged the row
   out of alignment. The grid columns were never the problem; they measure
   362/362 and always did.
   `anywhere` on top of normal wrapping is for D-059: once titles are
   admin-authored, a single pasted unbroken token would overflow again, and a
   dropdown must not be breakable by typing into a text field.
   The row's own `align-items:flex-start` (see .mega__i) is what keeps a
   two-line title from stretching its neighbour's hover chip. */
.mega__t{font-weight:700;font-size:15px;line-height:1.4;display:block;color:var(--ink);
  white-space:normal;overflow-wrap:anywhere}
.mega__d{font-size:12.5px;color:var(--mut);line-height:1.6;display:block;
  white-space:normal;overflow-wrap:anywhere}
/* `.menu a.mega__all`, not `.mega__all`. This row is an <a> inside .menu, so
   `.menu a{color:var(--nav-mut)}` above also matches it — and at (0,1,1) that
   selector BEAT the plain class at (0,1,0). The row was painted #cfe0f4, the
   pale blue the nav bar uses for idle links over the dark hero glass, on the
   panel's own #e7eaf1. Two near-whites: measured on the live page, the link was
   invisible in light mode and the owner reported it as a missing button.

   It only showed in light mode, and that is the tell: the dark override below
   is `html[data-theme="dark"] .mega__all`, which lands at (0,2,1) and happened
   to out-rank .menu a by accident. The light rule never had that luck. Both are
   pinned to the same shape now so neither can drift again. */
.menu a.mega__all{grid-column:1/-1;display:flex;align-items:center;justify-content:center;gap:9px;
  padding:15px;border-radius:18px;font-size:14px;font-weight:700;color:var(--navy);
  background:var(--bg3);transition:background .25s}
/* --navy is a brand token and does not flip with the theme, so on the dark
   panel's near-black --bg3 this row would be navy-on-black */
html[data-theme="dark"] .menu a.mega__all{color:var(--sky)}
/* :hover has the same trap one level deeper — `.menu a:hover` is (0,2,1) and
   the plain `.mega__all:hover` was (0,2,0), so the hover tint never landed
   either and the row lit up with the NAV's hover colour instead of its own. */
.menu a.mega__all:hover{color:var(--navy);background:rgba(0,66,141,.1)}
html[data-theme="dark"] .menu a.mega__all:hover{color:var(--sky);background:rgba(88,176,245,.14)}

.nav__cta{display:flex;align-items:center;gap:5px;flex-shrink:0}
.iconbtn{width:34px;height:34px;display:grid;place-items:center;border-radius:999px;cursor:pointer;
  background:transparent;color:var(--nav-mut,var(--mut));box-shadow:none;border:0;
  transition:color .25s,background .25s}
/* (owner · phone) Behind a pointer query, and that is the whole fix for the
   "dark circle" in the owner's screenshot: a touch screen latches :hover onto
   whatever the finger last landed on, so the burger kept this filled circle
   for as long as the drawer it opened stayed open — one icon in the row wearing
   a background none of its four neighbours had. A hover tint is a pointer
   affordance; a finger has no hover to preview with. */
@media (hover:hover){
  .iconbtn:hover{color:var(--nav-accent,var(--navy));background:var(--nav-hover,rgba(8,20,38,.07))}
}
.iconbtn .ico{width:17px;height:17px}
.lang{width:auto;padding:0 12px;font-family:var(--ff-m);font-size:11.5px;letter-spacing:.08em}
/* the bar's own buttons run compact */
.nav__cta .btn{padding:9px 18px;font-size:.86rem}
.nav__cta .btn .ico{width:15px;height:15px}
/* (D-054) The ghost CTA borrows the page's ink, which is invisible against
   the obsidian state — inside the bar it follows the bar's tokens instead.
   The primary CTA keeps --navy in both states: it is the one element that is
   meant to stay the brand's blue no matter what is behind the bar. */
.nav__cta .btn--ghost{color:var(--nav-ink);box-shadow:inset 0 0 0 1.6px var(--nav-hair)}
.nav__cta .btn--ghost:hover{color:var(--nav-accent);box-shadow:inset 0 0 0 1.6px var(--nav-accent)}

/* `justify-content`, and it is load-bearing rather than tidy: `.iconbtn` above
   centres its content with `place-items:center`, which is align-items plus
   justify-ITEMS — and a flex container ignores the second. So the three bars
   stacked from the TOP of the 34px button. Measured at 375: the bar stack's
   centre sat at y 57.1 while every icon beside it centred on y 66.1, a 9px
   lift, and it is most visible in the state the owner photographed — the open
   drawer, where those same three bars are the close X. */
.burger{display:none;flex-direction:column;justify-content:center;gap:5px}
.burger i{display:block;width:20px;height:2px;background:currentColor;border-radius:2px;transition:.35s var(--ease)}
.burger.is-open i:nth-child(1){transform:translateY(7px) rotate(45deg)}
.burger.is-open i:nth-child(2){opacity:0}
.burger.is-open i:nth-child(3){transform:translateY(-7px) rotate(-45deg)}

/* (D-054) The drawer is the bar's obsidian state taken full-screen, in BOTH
   themes: it is a transient overlay opened from a bar that is itself obsidian
   at the top of the page, and matching it is what makes the two read as one
   control rather than two. Literal colours rather than palette tokens for the
   same reason the cinema sections use them — this surface is fixed dark by
   design, so a theme flip must not wash it. */
/* (review · phone) `safe center`, not `center`, and it is the difference
   between a menu and a trap. This is a centred flex column inside a scroll
   container, and centred content that is TALLER than its container overflows at
   BOTH ends — the leading overflow has no scroll origin, so the first rows sit
   above the scrollport and cannot be reached by any gesture. The drawer grew
   past the viewport when #games and /blog joined it: 11 anchors x 67.0px
   (25.6px type x 1.72 line-height + 11px padding-block x2 + a 1px rule) + 10 x
   2px gap + 150px of vertical padding = 907px against a 390x844 phone. The
   rows that went out of reach were the top two — the home link and the courses
   link. `safe` drops the alignment to `start` for exactly the case where
   centring would push content out of view, and changes nothing when it fits.
   Written alone rather than after a `center` fallback on purpose: a browser
   that does not know `safe` drops the whole declaration and lands on the
   initial `flex-start`, which is the behaviour we want there anyway — the
   fallback would have handed those browsers back the bug. */
.drawer{position:fixed;inset:0;z-index:125;background:#04070e;color:#eaf2fc;
  display:flex;flex-direction:column;justify-content:safe center;gap:2px;padding:110px var(--pad) 40px;
  clip-path:inset(0 0 100% 0);transition:clip-path .7s var(--ease);overflow-y:auto}
.drawer.is-open{clip-path:inset(0 0 0 0)}
/* (owner · phone) The rule above says this surface "is the bar's obsidian state
   taken full-screen", and that is true only at the top of the page. Open the
   menu after scrolling and `is-stuck` has already swapped the bar to the page's
   own surface: measured at 375, an open drawer of #04070e capped by a bar
   painted rgb(255,255,255) with its icons at rgb(86,104,128) — a white header
   on a black sheet, which is the row the owner photographed as not lining up.
   The bar takes its obsidian tokens back for exactly as long as the drawer
   holds the body locked. Restated rather than shared with `.nav` above because
   the two are different states of one element, not a common base, and
   `.nav.is-stuck` here matches `html[data-theme="dark"] .nav.is-stuck` at
   (0,3,1) — equal weight, later in the file, so it wins in both themes. */
body.is-locked .nav.is-stuck{
  --nav-bg:rgba(4,7,14,.72);
  --nav-ink:#ffffff;
  --nav-mut:#cfe0f4;
  --nav-hair:rgba(88,176,245,.45);
  --nav-hover:rgba(255,255,255,.14);
  --nav-accent:var(--sky);
  --nav-accent-bg:rgba(88,176,245,.34);
  --nav-active-ink:#ffffff;
  --nav-shadow:none}
.drawer a{font-size:clamp(1.6rem,6.4vw,2.5rem);font-weight:800;font-family:var(--ff-d);
  padding-block:11px;border-block-end:1px solid rgba(88,176,245,.22);
  opacity:0;transform:translateY(20px);transition:color .25s}
.drawer a:hover{color:var(--sky)}
[lang="ar"] .drawer a{font-family:var(--ff-a);font-weight:400}
.drawer.is-open a{opacity:1;transform:none;transition:opacity .5s var(--ease),transform .5s var(--ease)}
.drawer.is-open a:nth-child(1){transition-delay:.16s}.drawer.is-open a:nth-child(2){transition-delay:.21s}
.drawer.is-open a:nth-child(3){transition-delay:.26s}.drawer.is-open a:nth-child(4){transition-delay:.31s}
.drawer.is-open a:nth-child(5){transition-delay:.36s}.drawer.is-open a:nth-child(6){transition-delay:.41s}
.drawer.is-open a:nth-child(7){transition-delay:.46s}.drawer.is-open a:nth-child(8){transition-delay:.51s}
/* (review · phone) …and on to 11, because the drawer now HAS 11 rows. The
   cadence stopped at 9 while #games, /blog and the auth fork pushed the list
   past it, and a row with no rule keeps transition-delay's initial 0 — so the
   last two rows did not arrive late, they arrived FIRST, a tenth of a second
   ahead of «الرئيسية» at the top. Same .05s step throughout. The signed-in
   drawer is the long one (8 section rows + /blog + dashboard + notifications,
   then the logout form as the 12th child), which is what sets 11 as the count
   to reach. */
.drawer.is-open a:nth-child(9){transition-delay:.56s}
.drawer.is-open a:nth-child(10){transition-delay:.61s}.drawer.is-open a:nth-child(11){transition-delay:.66s}
/* (D-046) The authed drawer carries a POST logout form as its 10th child, so
   the cadence needs one more beat and the form needs the entrance the
   sibling anchors get from `.drawer a` — a <form> matches none of those
   selectors, and without this it would sit at opacity 0 forever. */
.drawer form{opacity:0;transform:translateY(20px)}
/* .71s, one step past the 11th anchor above: the form is the LAST child of the
   signed-in drawer, and at .61s it used to arrive in a dead heat with the
   10th row. */
.drawer.is-open form{opacity:1;transform:none;transition-delay:.71s;
  transition:opacity .5s var(--ease),transform .5s var(--ease)}
.drawer form button{display:block;width:100%;text-align:start;cursor:pointer;
  border:0;border-block-end:1px solid rgba(88,176,245,.22);background:transparent;color:inherit;
  font-size:clamp(1.6rem,6.4vw,2.5rem);font-weight:800;font-family:var(--ff-d);padding-block:11px}
[lang="ar"] .drawer form button{font-family:var(--ff-a);font-weight:400}
/* the badge is sized for the 14px menu row; beside the drawer's display-size
   links it needs to grow with them or it reads as a stray dot */
.drawer .uchip__badge{font-size:1rem;padding:4px 13px;margin-inline-start:14px;vertical-align:middle}

/* …and `safe` alone would leave the menu SCROLLING on the commonest phone there
   is, which is a worse menu even though nothing is now unreachable. A short
   viewport gets a tighter rhythm and a shorter top pad so the whole list fits
   at once.

   THIS BLOCK MUST STAY BELOW `.drawer a`. It stood above it for one revision and
   did nothing at all: `.drawer a` and `@media{...} .drawer a` both weigh (0,1,1),
   so the plain rule won on source order and the padding stayed 11px. Measured in
   that state — 869px of content in an 844px viewport, the media query computing
   exactly nothing. Nothing about the arithmetic was wrong; the rule was simply
   never reached.

   The numbers, at the 25.6px type every phone resolves this clamp to (6.4vw
   loses to the 1.6rem floor below 400px): a row is 44.0px of line box + 6px of
   padding-block on each side + a 1px rule = 57.0px. Eleven of them is 627px,
   plus 10 x 2px of gap and 134px of vertical padding = 781px — inside 800, and
   360x800 is the binding phone, not 390x844.

   106px of top pad, not the 84 tried first, and it is measured rather than
   chosen: with the drawer open at the top of the page the sticky bar spans
   y 33.63 to 99.56 (65.94 tall, under the topbar), and it paints OVER the
   drawer — nav z-index 130 against the drawer's 125. At 84 the first row began
   at y 84 and `elementFromPoint` on it returned an `.iconbtn` from the bar:
   «الرئيسية» was sliced by the nav hairline. 106 clears 99.56 with 6.4px to
   spare.

   max-height:1000px, not 900: a 430x932 phone missed the query entirely and
   overflowed by 11px with the full 110/40 padding. At 1000 it is covered —
   27.5px type there, 60.3px rows, 818px inside 932.

   TWO CASES STILL SCROLL, both measured, both deliberately left to `safe`.
   · A SIGNED-IN visitor has a twelfth child — the logout form — and measures
     840px: fits 390x844 and 430x932, overflows 375x812 by 28 and 360x800 by 40.
     Making it fit would mean 4px rows for everybody, to serve one combination,
     and `safe` already lands the list at the top with nothing cut off (measured
     first-row top 106 in exactly that state).
   · A window at 1024x768 is ~1054px of rows — 40px type at the clamp's ceiling.
     Eleven display-size rows do not fit in 768px at any spacing, and shrinking
     the type to phone size on a desktop-width drawer costs more than the scroll.
   Scrolling is the correct outcome for a list genuinely taller than the screen.
   What `safe` guarantees is that it scrolls from the TOP — which is the whole
   difference between a long menu and a broken one. */
@media (max-height:1000px){
  .drawer{padding-block:106px 28px}
  .drawer a,.drawer form button{padding-block:6px}
}

/* ══════════════════════════════════ account menu · D-046 ═════════════════
   The signed-in face of the nav capsule: a pill in the same language as
   .btn--ghost beside it, opening a panel built like .mega (near-solid
   surface, light blur, --shadow2) so the two dropdowns in one header read
   as one system. Everything is logical-property based, so RTL and LTR both
   anchor the panel to the capsule's outer edge with no direction override. */
.uchip{position:relative;flex-shrink:0}
/* The standalone page shell has no drawer to hand the menu to, so its chip
   survives every breakpoint — and at 375 the row (brand + back link + chip +
   two toggles) measured 438px against a 375px viewport, pushing the language
   toggle past the edge. Allowed to take a second line instead: a guest's row
   still fits on one, so nothing about the existing header moves. */
.page__top-in{flex-wrap:wrap;row-gap:8px}
.uchip__btn{display:inline-flex;align-items:center;gap:8px;cursor:pointer;min-width:0;
  padding:9px 16px;border:0;border-radius:999px;background:transparent;
  font-family:inherit;font-size:.86rem;font-weight:600;color:var(--ink);
  box-shadow:inset 0 0 0 1.6px var(--line2);
  transition:color .25s,box-shadow .25s}
.uchip__btn:hover,.uchip.is-open .uchip__btn{color:var(--navy);box-shadow:inset 0 0 0 1.6px var(--navy)}
html[data-theme="dark"] .uchip__btn:hover,
html[data-theme="dark"] .uchip.is-open .uchip__btn{color:var(--sky);box-shadow:inset 0 0 0 1.6px var(--sky)}
/* (D-054) The chip is shared with the standalone page shell (.page__top),
   which has no obsidian state — so the bar's variant is scoped to .nav rather
   than folded into the base rule, and the page shell keeps the palette it has
   always had. */
.nav .uchip__btn{color:var(--nav-ink);box-shadow:inset 0 0 0 1.6px var(--nav-hair)}
.nav .uchip__btn:hover,.nav .uchip.is-open .uchip__btn{
  color:var(--nav-accent);box-shadow:inset 0 0 0 1.6px var(--nav-accent)}
.uchip__btn .ico{width:15px;height:15px;flex:0 0 auto}
.uchip__btn .ico:last-child{transition:transform .3s var(--ease)}
.uchip.is-open .uchip__btn .ico:last-child{transform:rotate(180deg)}
/* A full legal name (or a pasted one with no spaces) would otherwise stretch
   the capsule until the header wraps — one line, clipped, never wrapped. The
   cap scales with the viewport because .page__top keeps this chip at every
   width, phone included, where a fixed 210px would push the theme and
   language toggles off the row. */
.uchip__name{max-width:clamp(110px,17vw,150px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

.uchip__menu{position:absolute;inset-block-start:calc(100% + 12px);inset-inline-end:0;z-index:140;
  min-width:236px;padding:8px;border-radius:18px;display:grid;gap:2px;
  background:rgba(255,255,255,.96);
  backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);
  border:1px solid var(--line2);box-shadow:var(--shadow2);
  opacity:0;visibility:hidden;translate:0 10px;
  transition:opacity .3s var(--ease),translate .3s var(--ease),visibility .3s}
.uchip.is-open .uchip__menu{opacity:1;visibility:visible;translate:0 0}
html[data-theme="dark"] .uchip__menu{background:rgba(10,18,34,.96);border-color:rgba(255,255,255,.14)}
/* the page-wide reduced-motion block collapses the duration but leaves the
   10px offset to be crossed instantly, which still registers as a jump —
   reduced motion means the panel is simply there */
@media (prefers-reduced-motion:reduce){
  .uchip__menu{translate:none}
}
.uchip__i{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;
  padding:11px 14px;border:0;border-radius:12px;cursor:pointer;text-align:start;
  background:transparent;color:var(--ink);
  font-family:inherit;font-size:14px;font-weight:600;
  transition:color .25s,background .25s}
.uchip__i:hover,.uchip__i:focus-visible{background:var(--bg3);color:var(--navy)}
html[data-theme="dark"] .uchip__i:hover,
html[data-theme="dark"] .uchip__i:focus-visible{color:var(--sky)}
/* signing out is the one destructive row here, and it reads as such */
.uchip__i--out{color:var(--orange-dk)}
html[data-theme="dark"] .uchip__i--out{color:#ffb191}
.uchip__out{margin-block-start:5px;padding-block-start:5px;border-block-start:1px solid var(--line)}
/* the platform's badge pattern: accent pill, monospaced digits so 1 and 99
   sit at the same optical weight, never wider than its own row */
.uchip__badge{flex:0 0 auto;min-width:21px;padding:2px 7px;border-radius:999px;
  background:var(--orange);color:#2a0c02;text-align:center;
  font-family:var(--ff-m);font-size:11px;font-weight:600;line-height:1.6}

/* ══════════════════════════════════════════════════════════════ hero ═══ */
/* (anubi) The hero is two columns now — the academy's copy, and the sanctum
   Anubi performs in — where it used to be one column of type over a film.
   What went with the film: .hero__bg, .hero__vid, .hero__scrim and the glass
   .hero__mark. The scrim in particular has no job left: there is no
   photography under the type any more, only the obsidian ground, so the
   copy's contrast is a property of the background colour itself. */
.hero{position:relative;min-height:calc(100dvh - var(--head,124px));overflow:hidden;
  background:var(--au-ink)}
/* (anubi) The hieroglyph wall is BACK under the sanctum. The lab's hero stands
   on flat obsidian; the owner wants the new niche and the character on the
   platform's own scroll-scrubbed film, so the film, its no-video plate and the
   scrim that keeps the copy legible over it return exactly as they shipped.
   The sanctum paints its own opaque niche on top (z-index below), so the wall
   reads as the room the arch is cut into. */
.hero__bg{position:absolute;inset:-8% -4%;background-size:cover;background-position:center;
  will-change:transform;filter:saturate(1.05)}
.hero__vid{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  transform:scale(1.05);will-change:transform}
/* The autoplaying hero film. inset:-6% and a 1.06 scale rather than a flush
   inset:0, because heroMedia() slides it 10% of its own height on scroll and a
   flush element would show the page behind its top edge as it moved. */
.hero__film{position:absolute;inset:-6% -3%;width:106%;height:112%;object-fit:cover;
  transform:scale(1.06);will-change:transform;pointer-events:none}
.hero__scrim{position:absolute;inset:0;background:
  /* A band the height of the header, and it is not decoration. The nav is a
     translucent panel — rgba(4,7,14,.72) over backdrop-filter: blur(18px)
     saturate(1.65) — and a translucent panel shows whatever is behind it. Behind
     it used to be a scrim over a scroll-scrubbed film, which changed only when
     the visitor scrolled. Now it is an autoplaying video, so the panel's own
     rectangle changed contrast and colour 24 times a second while the page sat
     still: the header read as a box that flickered, and the saturate boost —
     tuned against a dark, static ground — amplified exactly the part that moved.
     The band gives it one ground again. It fades out by the header's own height,
     so the picture below is untouched. */
  linear-gradient(180deg,rgba(4,7,14,.94) 0,rgba(4,7,14,.62) calc(var(--head,124px) * .7),transparent var(--head,124px)),
  radial-gradient(115% 80% at 50% 34%,transparent 16%,rgba(4,7,14,.5) 62%,rgba(4,7,14,.92) 100%),
  linear-gradient(0deg,rgba(4,7,14,.97) 2%,rgba(4,7,14,.82) 34%,rgba(4,7,14,.3) 62%,rgba(4,7,14,.6) 100%)}

/* The one atmospheric layer the ground keeps: a slow bloom over the sanctum's
   side, so the two columns are lit as one room rather than sitting on flat
   black. Inline-end, which is the sanctum's side in both directions. */
.hero::before{content:'';position:absolute;inset:0;z-index:0;pointer-events:none;
  background:radial-gradient(72% 62% at 22% 18%,rgba(232,201,106,.10),transparent 62%),
             radial-gradient(90% 70% at 50% 108%,rgba(30,25,41,.9),transparent 66%)}
[dir="ltr"] .hero::before{
  background:radial-gradient(72% 62% at 78% 18%,rgba(232,201,106,.10),transparent 62%),
             radial-gradient(90% 70% at 50% 108%,rgba(30,25,41,.9),transparent 66%)}
/* The grid. Document order is copy → sanctum, so in RTL the sanctum lands at
   inline-end (the LEFT of the screen) exactly as the lab has it, with no
   direction-aware placement to keep in sync. `min-height` and the flex
   justify-content it replaced stay expressed as `align-content:center`: the
   copy used to hang off the bottom edge of a full-bleed film, and centred is
   what reads right beside a standing figure. */
/* One column. The second one held the sanctum, and the sanctum is gone -- the
   hero's picture is the film behind the copy now, not a figure beside it. Left
   as a grid rather than flattened to a block so the vertical centring and the
   gap below keep working unchanged. */
.hero__in{position:relative;z-index:5;min-height:calc(100dvh - var(--head,124px));
  display:grid;grid-template-columns:minmax(0,1fr);
  align-items:center;align-content:center;gap:clamp(28px,4.4vw,64px);
  padding-block:clamp(40px,6vh,80px) clamp(28px,4vh,54px);
  max-width:var(--maxw);margin-inline:auto;padding-inline:var(--pad)}
.hero__copy{min-width:0}
/* Stacked, COPY first. The lab put its statue at the top of a phone because the
   lab was about the statue; this page is about the academy, and the H1 that names
   it — the one line a search engine and a first-time visitor both read first —
   must not sit below a 74dvh picture. The statue follows immediately, still the
   first thing the eye lands on once the sentence is read. */
@media (max-width:900px){
  .hero__in{gap:clamp(22px,4vh,38px)}
}

/* (anubi) The kicker is gold, not sky: it is the only lettering that sits at
   the same optical weight as the jamb glyphs opposite it, and a blue line
   there was the one thing on the new ground that still read as the old page. */
.hero__k{font-family:var(--ff-m);font-weight:500;font-size:11.5px;letter-spacing:.24em;
  color:var(--au-gold-500);margin-block-end:26px;display:block}
[lang="ar"] .hero__k{font-family:var(--ff-b);font-weight:700;letter-spacing:.06em;font-size:14px}
/* Amiri, in both languages. The headline is the one place the lab's display
   face earns its download — the sanctum beside it is carved, not printed —
   and Amiri covers Arabic AND Latin, so the EN swap does not fall back to a
   different face mid-sentence the way Instrument Serif did.
   The size clamps and the two line-heights are the site's own, untouched:
   they are tuned to the word masks site.js animates, and a headline that
   changed height would land the entrance on a different baseline. */
.hero__t{font-size:clamp(2.05rem,4.4vw,4.2rem);line-height:1.14;letter-spacing:-.02em;font-weight:700;
  font-family:var(--au-ff-dis);color:var(--au-papyrus)}
[lang="ar"] .hero__t{font-family:var(--au-ff-dis);font-weight:700;line-height:1.45;letter-spacing:0;
  font-size:clamp(1.8rem,3.5vw,3.2rem)}
.hero__s{margin-block-start:22px;color:rgba(239,228,205,.72);
  font-size:clamp(.99rem,1vw,1.1rem);line-height:1.8;max-width:56ch}
/* No rule above the buttons. It was a hairline that separated the lede from the
   actions while the copy sat in a half-width column beside the sanctum — a short
   line inside a block of text reads as typography. With the sanctum gone the copy
   column is the full width of the page, so the same 1px ran edge to edge across
   the hero film and read as a seam in the video rather than as a divider. The
   spacing it used to sit on stays; only the stroke is gone. */
.hero__row{display:flex;align-items:center;gap:16px;flex-wrap:wrap;
  margin-block-start:clamp(22px,2.8vh,36px);padding-block-start:clamp(20px,2.4vh,30px)}
/* (D-054) Identity chips. The figures used to float in a bare row that ran
   under the scroll cue; each now sits in its own hairline capsule, which both
   gives the row a shape and makes the collision impossible to reintroduce —
   there is no longer an absolutely-positioned element in that corner.
   (anubi) `flex-basis:100%` puts them on their own line: the copy column is
   half the width it was, and four capsules beside two buttons on one line
   collapsed each figure onto three lines of its own. */
.hero__stats{display:flex;gap:clamp(8px,.9vw,14px);flex-wrap:wrap;
  flex:1 0 100%;margin-block-start:4px}
.hero__stat{padding:10px clamp(13px,1.1vw,20px);border-radius:14px;
  border:1px solid var(--au-line);background:rgba(18,15,24,.6);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  transition:border-color .35s var(--ease),background .35s var(--ease)}
.hero__stat:hover{border-color:var(--au-gold-600);background:rgba(201,162,39,.09)}
.hero__stat b{display:block;font-family:var(--ff-m);font-weight:600;
  font-size:clamp(1.2rem,1.5vw,1.6rem);line-height:1;color:var(--au-gold-300);
  direction:ltr;unicode-bidi:isolate}
.hero__stat span{font-size:11.5px;letter-spacing:.04em;color:rgba(239,228,205,.5);
  display:block;margin-block-start:6px;white-space:nowrap}

/* ═══════════════════════════════════════════════════════════ sanctum ════ */
/* (anubi) The niche, verbatim from the lab in structure and in feel. Every
   rule is scoped under `.sanctum` — that is not tidiness, it is required:
   `.glyphs` is already a class on this page (the cartouche puzzle), and a
   bare `.glyphs{flex-direction:column}` here would have turned that widget's
   row of hieroglyph tiles into a column. `.jamb`, `.shaft`, `.plinth` and
   `#dust` are scoped for the same reason even though they collide with
   nothing today.
   The sprite: sized to nothing, but NOT display:none — a hidden <svg> makes
   its <symbol>s unreferenceable in some engines. */
.au-sprite{position:absolute;width:0;height:0;overflow:hidden}

/* height-first, not width-first: with `width:100%` resolved before the ratio,
   a max-height clamp on a short viewport won the cascade and stretched the arch
   wider than 5:6. Driving the box from its height keeps the ratio exact and lets
   max-width do the narrowing on a phone. */
/* The width is the SMALLEST of three limits — the column, the design cap, and
   whatever the height ceiling allows at 5:6 — so the ratio derives the height and
   nothing ever clamps it afterwards. Clamping height and width independently is
   what broke the arch: on a 390px phone the height cap wanted 521px of width, the
   column gave 390, and the box came out 0.57 wide instead of 0.83. */
.sanctum{position:relative;aspect-ratio:5/6;
  width:min(100%,520px,calc(min(74dvh,660px) * 5 / 6));height:auto;
  margin-inline:auto;
  border-radius:200px 200px 22px 22px;
  /* (owner, 2026-09-08) Not the lab's smoky gold glow: the niche is cut into the
     SAME hieroglyph stone the hero stands on — the film's own poster frame,
     darkened toward the floor so the figure reads against it and the gold of
     the statue stays the brightest thing in the arch. One asset already on the
     page, no new download. */
  background:
    linear-gradient(to bottom,rgba(6,5,10,.42) 0%,rgba(6,5,10,.62) 45%,rgba(6,5,10,.90) 82%,#060409 100%),
    radial-gradient(70% 60% at 50% 30%,transparent 40%,rgba(6,5,10,.55) 100%),
    url("../video/hero-poster.jpg") center 40%/cover no-repeat,
    #0a0810;
  border:1px solid var(--au-line);
  box-shadow:inset 0 0 90px rgba(0,0,0,.9),0 40px 90px -40px #000;
  overflow:hidden;isolation:isolate}
.sanctum .jamb{position:absolute;top:14%;bottom:0;width:34px;opacity:.5;
  background:repeating-linear-gradient(to bottom,transparent 0 8px,rgba(201,162,39,.16) 8px 9px)}
.sanctum .jamb.a{inset-inline-start:16px}
.sanctum .jamb.b{inset-inline-end:16px}
.sanctum .glyphs{direction:inherit;position:absolute;top:16%;bottom:2%;width:30px;
  display:flex;flex-direction:column;gap:22px;align-items:center;
  opacity:.42;color:var(--au-gold-500)}
.sanctum .glyphs.a{inset-inline-start:18px}
.sanctum .glyphs.b{inset-inline-end:18px}
.sanctum .glyphs svg{width:17px;height:17px;flex:none}
/* The shaft of light from the opening above the niche. Physically anchored
   with left/translateX rather than an inset-inline pair: it is centred, so
   there is no direction to honour, and a logical property here would only
   invite a mirrored translate that cancels itself. */
.sanctum .shaft{position:absolute;top:-12%;left:50%;width:56%;height:96%;
  transform:translateX(-50%);pointer-events:none;filter:blur(16px);
  display:none; /* (owner, 2026-09-08) the blurred shaft read as smoke */
  background:linear-gradient(to bottom,rgba(244,223,160,.20),rgba(244,223,160,.05) 45%,transparent 78%);
  animation:auShaft 9s var(--au-io) infinite}
@keyframes auShaft{
  0%,100%{opacity:.75;transform:translateX(-50%) scaleX(1)}
  50%{opacity:1;transform:translateX(-50%) scaleX(1.09)}}
.sanctum #dust{position:absolute;inset:0;width:100%;height:100%;
  pointer-events:none;mix-blend-mode:screen}
/* The contact shadow the statue's feet land on. anubi-hero.js puts the feet
   at 92.5% of the niche's height, which is where this sits. */
.sanctum .plinth{position:absolute;left:50%;bottom:5.5%;transform:translateX(-50%);
  width:56%;height:26px;border-radius:50%;filter:blur(5px);
  background:radial-gradient(closest-side,rgba(0,0,0,.85),transparent 72%)}
  /* The levitation's ribbons sweep FAR wider than the figure. Measured on the
     levitate take: the man is about 326 of the frame's 1092 source pixels, but
     the golden streamers reach both edges, so at the companion's 224px box the
     frame's own 358px landed 67px outside on each side and the ribbons ended in
     a hard vertical cut. Every one of the three canvases is narrower than that
     frame -- the hero's 506 against 768, the loader's likewise -- so this belongs
     on all of them, not on the small one.
     A fade rather than a wider box: widening the companion's box would push the
     figure 67px short of each screen edge on its walk (clampPet works from the
     box, not the figure) and would stretch the drag grip with it. The mask is
     tied to the box in percentages so it scales with every breakpoint, and the
     outer 12% is empty on every clip -- the figure's own margin is never less
     than 43px of 224, or 138 of 506. The bottom is deliberately NOT faded: the
     feet and the shadow live there. */
.sanctum .stage-canvas{position:absolute;inset:0;width:100%;height:100%;-webkit-mask-image:linear-gradient(to right,transparent,#000 12%,#000 88%,transparent),linear-gradient(to bottom,transparent,#000 8%,#000);-webkit-mask-composite:source-in;mask-image:linear-gradient(to right,transparent,#000 12%,#000 88%,transparent),linear-gradient(to bottom,transparent,#000 8%,#000);mask-composite:intersect}

@media (prefers-reduced-motion:reduce){
  .sanctum .shaft{animation:none}
}


/* the word-by-word entrance */
.sw{display:inline-block;overflow:hidden;vertical-align:bottom}
.sw > span{display:inline-block;transform:translateY(110%)}

/* (D-054 · library entry 43 "DesignPro Academy", ShinyText) The mechanic is
   taken exactly as the entry specifies — a wide multi-stop gradient painted on
   the element, `background-size:200% auto`, clipped to the glyphs, and swept
   by animating background-position -200% → 200% on a 6s linear loop. The
   entry's own colours are not: theirs is a cyan-on-navy brand, ours is the
   near-white headline the hero has always had with a white-and-sky band
   passing through it, so the line never drops below its reading contrast.
   GUARD: transparent text with an unsupported clip is an INVISIBLE headline.
   The colour is only given away inside @supports, so an engine that cannot
   clip keeps the plain white heading it had before. */
/* EXACTLY ONE element per word may carry the clip, and it must be the
   innermost one — the `.rc` reveal unit site.js splits each word into.
   Two failures were walked to get here and both are worth remembering:
     · On the h1: `.sw` is an inline-block with overflow:hidden, and an
       inline-block's text does not join an ancestor's text clip. The gradient
       clipped to nothing and the headline VANISHED.
     · On `.sw > span` AND `.rc` together: Chrome clips the parent's gradient
       to its descendant's glyphs at their untransformed layout position while
       the child paints the same glyphs again at its animated offset — every
       word rendered TWICE, ghosted apart by the entrance's translate. That is
       the doubling reported from a screenshot.
   Only `.rc` is styled now. Before the splitter runs — and if GSAP never
   loads, or under reduced motion, where it never runs at all — the text stays
   in `.sw > span` with no gradient and simply inherits the headline's own
   white. That is the same solid headline this page had before the sweep
   existed, which makes it a safe floor rather than a fallback to nothing. */
@supports ((background-clip:text) or (-webkit-background-clip:text)){
  .shiny .rc{
    /* One narrow pure-white hot band on the base ink — no colour tint. The
       earlier sky-blue mid-stop lit scattered letters a DIFFERENT hue, which
       read as misregistered ghost copies against the busy hieroglyph film
       (user report, verified by two-frame comparison at the gate). */
    background-image:linear-gradient(to right,
      #eaf2fc 0%,#eaf2fc 46%,#ffffff 50%,#eaf2fc 54%,#eaf2fc 100%);
    background-size:200% auto;
    -webkit-background-clip:text;background-clip:text;
    color:transparent;-webkit-text-fill-color:transparent;
    animation:shine 6s linear infinite;animation-delay:var(--sd,0s)}
  /* Each word starts its sweep a beat after the one before it, so the band
     reads as travelling along the line instead of every word flashing at
     once. The offset is negative — the animation starts already part-way in,
     which is what keeps the cascade running from the very first loop rather
     than leaving the later words blank for a second. Word order follows
     document order, which is the reading direction in both RTL and LTR. */
  .shiny .sw{--sd:0s}
  .shiny .sw:nth-of-type(2){--sd:-.34s}
  .shiny .sw:nth-of-type(3){--sd:-.68s}
  .shiny .sw:nth-of-type(4){--sd:-1.02s}
  .shiny .sw:nth-of-type(5){--sd:-1.36s}
  .shiny .sw:nth-of-type(6){--sd:-1.7s}
  .shiny .sw:nth-of-type(7){--sd:-2.04s}
  .shiny .sw:nth-of-type(n+8){--sd:-2.38s}
}
@keyframes shine{0%{background-position:-200% center}100%{background-position:200% center}}

/* ═══════════════════════════════════════════════════════════ marquee ═══ */
.strip{border-block:1px solid var(--line);overflow:hidden;background:var(--bg2);
  padding-block:14px;direction:ltr}
.strip__t{display:flex;gap:40px;align-items:center;width:max-content;direction:ltr;will-change:transform}
.strip__t span{font-family:var(--ff-m);font-weight:500;font-size:11.5px;text-transform:uppercase;
  letter-spacing:.26em;color:var(--mut);
  white-space:nowrap}
.strip__t span:nth-child(3n+2){color:var(--orange)}

/* ═════════════════════════════════════════════════════════════ about ═══ */
.about__top{display:grid;grid-template-columns:1.2fr .8fr;gap:clamp(28px,5vw,110px);
  align-items:end;padding-block-end:clamp(34px,4vw,64px)}
.vm{display:grid;grid-template-columns:repeat(2,1fr);gap:16px;margin-block-start:clamp(32px,3.6vw,56px)}
.vm__c{padding:clamp(26px,2.5vw,40px);border-radius:24px;background:var(--bg2);
  box-shadow:inset 0 0 0 1px var(--line);position:relative;overflow:hidden}
.vm__c::after{content:'';position:absolute;inset-block-start:0;inset-inline-start:0;width:100%;height:3px;
  background:linear-gradient(90deg,var(--navy),var(--orange))}
.vm__c h3{font-size:1.12rem;margin-block-end:13px;display:flex;align-items:center;gap:12px}
.vm__c h3 .ico{width:22px;height:22px;color:var(--azure)}
.vm__c p{color:var(--mut);font-size:1.03rem;line-height:1.85}

.values{display:grid;grid-template-columns:repeat(6,1fr);
  border-block-start:1px solid var(--line);margin-block-start:clamp(40px,4.4vw,72px)}
.val{position:relative;padding:clamp(24px,2vw,36px) clamp(16px,1.4vw,26px);
  border-inline-end:1px solid var(--line);transition:background .35s}
.val:last-child{border-inline-end:0}
.val:hover{background:var(--bg2)}
/* (C2) a top accent that sweeps in from the reading edge on hover — a small
   directional cue that costs nothing beyond the existing background fade */
.val::before{content:'';position:absolute;inset-block-start:-1px;inset-inline:0;height:2px;
  background:linear-gradient(90deg,var(--navy),var(--orange));
  transform:scaleX(0);transform-origin:0 50%;transition:transform .5s var(--ease)}
[dir="rtl"] .val::before{transform-origin:100% 50%}
.val:hover::before{transform:scaleX(1)}
.val i{font-family:var(--ff-m);font-size:13px;color:var(--azure);font-style:normal;
  display:block;margin-block-end:15px}
.val b{display:block;font-size:1.08rem;margin-block-end:9px}
.val span{font-size:.93rem;color:var(--mut);line-height:1.7}

.stats{display:grid;grid-template-columns:repeat(5,1fr);
  border-block:1px solid var(--line);background:var(--bg2)}
.stat{padding:clamp(30px,3vw,52px) clamp(14px,1.6vw,28px);text-align:center;
  border-inline-end:1px solid var(--line)}
.stat:last-child{border-inline-end:0}
.stat b{display:block;font-family:var(--ff-m);font-weight:600;font-size:clamp(1.6rem,2.2vw,2.3rem);line-height:1;
  background:linear-gradient(160deg,var(--navy),var(--sapphire) 55%,var(--orange));
  background-size:220% 100%;background-position:0 0;
  -webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.stat span{display:block;margin-block-start:13px;font-size:13.5px;color:var(--mut)}
/* (C3) one-shot sheen across the gradient number once the stat has entered
   view — reveal() already flips .rv to .is-in for this element, so no new
   JS trigger is needed, just a keyframe hung off the class it already gets */
@keyframes statSheen{from{background-position:130% 0}to{background-position:0 0}}
.stat.is-in b{animation:statSheen 1.1s var(--ease2) .25s 1 both}

/* ══════════════════════════════ courses — sticky scroll reveal (#126) ═══
   (D-040) The sticky card stack and its 3D tilt are gone. The tracks are an
   editorial flow now — no boxes, hairline separators — with a panel beside
   them that stays put and swaps to the identity of whichever track the
   reader is level with. The panel is `position:sticky`, deliberately NOT a
   ScrollTrigger pin: a pin lengthens the document and feeds back into every
   other pin's measurement (see the note above revealSection() in site.js). */
.tracks{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(0,.65fr);
  gap:clamp(30px,4vw,80px);align-items:start}
.tracks__flow{display:grid}
.track{padding-block:clamp(38px,4.4vw,72px);border-block-start:1px solid var(--line);
  position:relative}
.track:first-child{border-block-start:0;padding-block-start:0}
/* the accent hairline that marks the block the panel is currently showing */
.track::before{content:'';position:absolute;inset-block-start:-1px;inset-inline:0;height:1px;
  background:var(--accent);transform:scaleX(0);transform-origin:0 50%;
  transition:transform .5s var(--ease)}
[dir="rtl"] .track::before{transform-origin:100% 50%}
.track.is-on::before{transform:scaleX(1)}
.track:first-child::before{content:none}
.track__head{display:flex;align-items:baseline;gap:clamp(14px,1.6vw,26px)}
/* ── (D-057) the lifted accent companion ───────────────────────────────────
   Each track carries its own `--accent` as a literal hex from config/courses:
   #0f52ba, #00428d, #ff6b35, #003366, #1f9cf0. Those are BRAND shades, so they
   do not flip with the theme the way the semantic tokens do — and three of
   them are dark blues that all but vanish on the obsidian dark ground.
   Measured before the fix: the 144px `.tpanel__n` numeral 2.82:1, `.mod__n`
   2.68:1, both below their floors.
   `--accent-lift` keeps each track's HUE and moves only its luminance AWAY
   from whatever ground it is on, so the five tracks stay distinguishable from
   one another while every one of them clears the floor. It is declared on the
   same elements that carry `--accent` and inherits down to the numerals,
   icons and codes inside them.

   BOTH directions are needed, and the second was found by auditing every
   matching element rather than the first few: the light theme has the mirror
   problem, and it is worse. The graphic-design track's #ff6b35 on white
   measured 2.34–2.84:1 across EIGHT element classes — the tracks panel
   numeral, the module index, the track list icons, the card code, the chips,
   the aside list icons and the nav dropdown numbers. So the light value
   deepens toward ink instead of lifting toward paper. 62% is the shallowest
   deepening that clears 4.5:1 everywhere it is used. The binding case is NOT
   plain white — it is accent text sitting on its OWN 12%-accent-tinted chip
   (`.mod__n`, `.mega__n`), where the tint darkens the ground just enough to
   eat the margin: at 68% those two measured 4.33 and 4.38, the only two left
   failing. At 62% they are 4.91 and 4.96, and #ff6b35 on pure white is
   5.94:1.

   Only TEXT and ICONS take the lift. Backgrounds — `.tab.is-on`, the tinted
   `.mod__n` and `.mega__n` chips — keep the raw `--accent`, because those
   carry white type and need the vivid brand colour behind it. */
/* `.page__body--track` is /courses/{slug}: the standalone page reuses `.box`
   and `.box li .ico` — the aside's tick and arrow lists — which read
   `--accent-lift` and nothing else. It declares `--accent` inline from the
   track's own colour but matched none of the five selectors below, so every
   icon in that aside fell back to the muted default on a page whose whole
   identity is the track's colour. The class is only present when the accent
   really was declared (see the note in course.blade.php): `--accent` has no
   value anywhere in this stylesheet, and color-mix() over an undefined custom
   property is invalid at computed-value time, which would leave --accent-lift
   unset on the inner pages that have no track rather than falling back. */
.track,.tpanel,.pane,.mega__i,.tab,.page__body--track{--accent-lift:color-mix(in srgb,var(--accent) 62%,#081426)}
html[data-theme="dark"] .track,html[data-theme="dark"] .tpanel,
html[data-theme="dark"] .pane,html[data-theme="dark"] .mega__i,
html[data-theme="dark"] .tab,
html[data-theme="dark"] .page__body--track{--accent-lift:color-mix(in srgb,var(--accent) 50%,#eaf2fc)}

/* The idle numeral is deliberately faint — the active one is what lights up —
   but it used to borrow `--line2`, a BORDER token, whose alpha is tuned for
   hairlines and not for 35px type: 1.47:1 on paper and 1.66:1 on obsidian.
   That is not faint, it is gone. Its own alpha per theme keeps it clearly
   secondary while staying legible — 3.4:1 light, 4.1:1 dark, floor 3. */
.track__n{font-family:var(--ff-m);font-weight:600;font-size:clamp(1.5rem,2.4vw,2.3rem);
  line-height:1;color:rgba(8,20,38,.50);transition:color .45s var(--ease)}
html[data-theme="dark"] .track__n{color:rgba(234,242,252,.45)}
.track.is-on .track__n{color:var(--accent-lift)}
.track__id{min-width:0}
.track__t{font-size:clamp(1.35rem,2vw,2rem);margin-block-start:9px}
.track__tag{color:var(--mut);font-size:1.04rem;line-height:1.85;max-width:62ch;
  margin-block-start:14px}
.track__list{display:grid;gap:11px;margin-block-start:26px;max-width:70ch}
.track__list li{display:flex;gap:12px;font-size:1.01rem;line-height:1.65;list-style:none}
.track__list .ico{width:18px;height:18px;flex-shrink:0;margin-block-start:5px;color:var(--accent-lift)}
.track__acts{display:flex;gap:11px;flex-wrap:wrap;margin-block-start:28px}

/* the sticky identity panel — one face per track, all stacked in the same
   grid cell so the panel's height is the tallest face and nothing reflows
   as the faces swap */
.tracks__panel{position:sticky;inset-block-start:110px;display:grid}
.tpanel{grid-area:1/1;border-radius:24px;padding:clamp(24px,2.4vw,36px);
  background:color-mix(in srgb,var(--accent) 10%,var(--bg2));
  /* (#89) a static luminous edge tint — the border beam's look without an
     infinite animation running behind five panels for the whole session */
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 38%,transparent),
             0 18px 50px color-mix(in srgb,var(--accent) 14%,transparent);
  opacity:0;translate:0 12px;pointer-events:none;
  transition:opacity .45s var(--ease),translate .45s var(--ease)}
.tpanel.is-on{opacity:1;translate:0 0}
.tpanel__n{display:block;font-family:var(--ff-m);font-weight:600;font-size:9rem;line-height:.82;
  letter-spacing:-.04em;color:var(--accent-lift);opacity:.9}
.tpanel .card__code{display:block;margin-block-start:14px}
/* the face's only line of prose; clamped so a long tagline cannot grow the
   tallest face and push the sticky box past the viewport it has to fit in */
.tpanel__tag{margin-block-start:12px;color:var(--mut);font-size:.96rem;line-height:1.8;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden}

@media (max-width:1000px){
  /* single column: the flow blocks are self-contained, so the panel — which
     only ever mirrored them — is dropped rather than stacked on top */
  .tracks{grid-template-columns:1fr}
  .tracks__panel{display:none}
}

.card__code{font-family:var(--ff-m);font-size:12px;letter-spacing:.12em;color:var(--accent-lift)}
.card__meta{display:flex;flex-wrap:wrap;gap:9px;margin-block-start:26px}
.chip{display:inline-flex;align-items:center;gap:8px;padding:10px 17px;border-radius:999px;
  font-size:13.5px;color:var(--mut);box-shadow:inset 0 0 0 1px var(--line);background:var(--bg)}
.chip .ico{width:15px;height:15px;color:var(--accent-lift)}
.chip b{color:var(--ink);font-weight:700}
.card__tools{display:flex;flex-wrap:wrap;gap:7px;margin-block-start:24px}
.tool{font-family:var(--ff-m);font-size:11.5px;padding:7px 13px;border-radius:9px;
  color:var(--mut);background:var(--bg3)}

/* ═══════════════════════════════ brand identity placements (D-052) ═════ */
/* The about section's brand card — the hologram film resolving into the
   lockup. Fixed near-black rather than a theme token: the film is graded for
   black, and on the light theme a --bg2 frame around it would ring the
   footage in white. `aspect-ratio` reserves the box before a single byte of
   video arrives, so the card never collapses and reflows the values grid
   under it while the visitor is reading. */
.bfilm{position:relative;overflow:hidden;border-radius:20px;background:var(--dark);
  border:1px solid var(--line);box-shadow:var(--shadow);
  margin-block-start:clamp(34px,4vw,60px);aspect-ratio:16/9}
.bfilm__v{position:absolute;inset:0;width:100%;height:100%;max-width:none;
  object-fit:cover;display:block}
@media (max-width:760px){.bfilm{aspect-ratio:4/3}}

/* The full-bleed identity interlude: the brand's cover artwork, given a band
   of its own between the argument and the FAQ. Always dark in both themes —
   the artwork is graded for black. (D-055: this was briefly a two-instance
   component, the second being a pre-footer film band; the user rejected that
   band, so the film-specific rules went with it rather than sitting here
   unused.) */
.iband{position:relative;display:block;overflow:hidden;background:var(--dark);
  border-block:1px solid var(--line)}
.iband__img{display:block;width:100%;max-width:none;object-fit:cover;height:auto}
/* The cover is a 2.6:1 scroll on a black field. Letting it keep its own ratio
   on wide screens shows the artwork whole; on narrow ones that would leave a
   40px sliver, so below the breakpoint the band takes a fixed height and
   crops toward the centre instead. */
@media (max-width:760px){.iband__img{height:clamp(180px,44vw,260px)}}

/* ══════════════════════════════════════════ full-bleed image sections ══ */
.reveal{position:relative;min-height:100vh;min-height:100dvh;overflow:hidden;background:#04070e}
.reveal__bg{position:absolute;inset:-10% -5%;background-size:cover;background-position:center;
  will-change:transform}
/* the global img,video{max-width:100%} rule fights the oversized inset box,
   so the video states its overscan box explicitly (the parallax needs it) */
video.reveal__bg{inset:auto;top:-10%;left:-5%;width:110%;height:120%;
  max-width:none;object-fit:cover}
.reveal__scrim{position:absolute;inset:0;background:
  linear-gradient(270deg,rgba(4,7,14,.94),rgba(4,7,14,.32) 60%,rgba(4,7,14,.7)),
  linear-gradient(0deg,rgba(4,7,14,.86),transparent 46%)}
[dir="ltr"] .reveal__scrim{background:
  linear-gradient(90deg,rgba(4,7,14,.94),rgba(4,7,14,.32) 60%,rgba(4,7,14,.7)),
  linear-gradient(0deg,rgba(4,7,14,.86),transparent 46%)}
.reveal__in{position:relative;z-index:4;min-height:100vh;min-height:100dvh;display:flex;
  align-items:center;max-width:var(--maxw);margin-inline:auto;padding-inline:var(--pad)}
.sbeats{position:relative;min-height:clamp(230px,34vh,350px);flex:0 0 auto;width:min(820px,100%)}
.sbeat{position:absolute;inset-block-start:0;inset-inline-start:0;width:100%;
  opacity:0;visibility:hidden;transform:translateY(16px);
  transition:opacity .5s var(--ease),transform .5s var(--ease)}
.sbeat.is-on{opacity:1;visibility:visible;transform:none}
.sbeat h3{font-size:clamp(1.5rem,2.4vw,2.2rem);margin-block:15px 16px;letter-spacing:-.022em}
.sbeat p{color:rgba(234,242,252,.8);font-size:clamp(.99rem,1vw,1.1rem);line-height:1.85}
.sbeat__n{font-family:var(--ff-m);font-size:12px;letter-spacing:.3em;color:var(--sky);
  direction:ltr;unicode-bidi:isolate;display:inline-block}

/* (D-040) the beat progress rail — three nodes on a line whose fill tracks
   the pinned scroll. `--fill` is written by revealSection() in site.js and
   defaults to 0, so a no-JS visitor sees a plain unfilled rail rather than
   a broken one. The colours are literal because this rail only ever sits on
   the fixed near-black of a `.reveal` section, in both themes. */
.srail{position:relative;flex:0 0 auto;display:flex;flex-direction:column;
  justify-content:space-between;align-self:stretch;
  margin-inline-end:clamp(20px,2.6vw,48px);margin-block:clamp(40px,8vh,90px);
  padding-inline-start:26px}
.srail__line{position:absolute;inset-block:4px;inset-inline-start:5px;width:2px;
  background:rgba(234,242,252,.18);border-radius:2px}
.srail__line i{position:absolute;inset-block-start:0;inset-inline-start:0;width:100%;
  height:var(--fill,0);border-radius:2px;
  background:linear-gradient(180deg,var(--sky),var(--orange));
  transition:height .35s var(--ease)}
.srail__n{position:relative;font-family:var(--ff-m);font-size:11px;letter-spacing:.18em;
  color:rgba(234,242,252,.42);transition:color .4s var(--ease)}
.srail__n::before{content:'';position:absolute;inset-block-start:50%;inset-inline-start:-26px;
  translate:0 -50%;width:12px;height:12px;border-radius:50%;
  background:#04070e;box-shadow:inset 0 0 0 2px rgba(234,242,252,.28);
  transition:box-shadow .4s var(--ease)}
.srail__n.is-on{color:#fff}
.srail__n.is-on::before{box-shadow:inset 0 0 0 4px var(--orange),0 0 14px 3px rgba(255,107,53,.45)}
@media (max-width:900px){.srail{display:none}}

/* ══════════════════════════════════════════════════════════ curriculum ═ */
/* .tabs is the scroll box and the positioning context for the highlight
   pill; .tabs__rail carries role="tablist" and the flex row, so the pill can
   live inside the scroll box (and therefore travel with it) without being an
   invalid non-tab child of the tablist */
/* (public-ux · strip) MEASURED, not guessed: with the live five-track
   catalogue the rail's natural width is 1678px in English and 1477px in
   Arabic, against a 1321px container at a 1440px viewport. The five titles
   have therefore never fitted one row on ANY laptop — and the old fade
   below only fired under 1240px, i.e. nowhere near where the clipping
   actually starts. That is why the owner saw a fifth tab cut off at the
   viewport edge with no way to reach it.
   Hence two modes, chosen by width:
     ≥960  the rail WRAPS. Nothing is clipped, so no affordance is needed
           and the fifth track is simply on screen. This is the desktop
           answer: an edge fade only advertises hidden content; wrapping
           removes the hiding. tabsPill() already positions the highlight
           from offsetTop as well as offsetLeft, so the pill lands correctly
           on a second row — verified by measurement, see site.js.
     <960  the rail stays one line and scrolls, with the fade + wheel +
           scroll-into-view affordances below and in site.js.
   960 is where wrapping stops paying for itself, measured on the English
   titles (the wider of the two languages): rows are 2 at 1440, 1280, 1024
   AND 960 — then 3 at 900 and 4 at 820, i.e. a 213px wall of pills above
   the content. Two rows is a tidy strip; four is a menu. Below 960 is also
   where the device is usually a touch one, and a swipe is the native way to
   move a strip there — which is exactly what the fade advertises. */
.tabs{position:relative;padding-block-end:6px;
  margin-block:clamp(30px,3vw,50px) clamp(26px,2.6vw,42px)}
.tabs__rail{display:flex;flex-wrap:wrap;gap:9px}
.tabs::-webkit-scrollbar{display:none}
.tab{position:relative;flex-shrink:0;display:flex;align-items:center;gap:10px;padding:13px 22px;border:0;
  border-radius:999px;background:var(--bg2);cursor:pointer;font-size:14px;font-weight:600;color:var(--mut);
  box-shadow:inset 0 0 0 1px var(--line);transition:.3s var(--ease);white-space:nowrap}
.tab:hover{color:var(--ink);box-shadow:inset 0 0 0 1px var(--line2)}
.tab.is-on{color:#fff;background:var(--accent);box-shadow:none}
.tab em{font-family:var(--ff-m);font-style:normal;font-size:12px;opacity:.8}
/* (C4) a pill that physically slides under the active tab instead of each
   tab swapping its own background — .tab keeps its own .is-on background as
   the no-JS/fallback look; once has-pill lands, the active tab goes
   transparent and the pill (one shared element) carries the colour and the
   motion so the highlight itself glides between tabs */
.tab>*{position:relative;z-index:2}
.tabs__pill{position:absolute;z-index:1;border-radius:999px;background:var(--azure);
  transition:left .55s var(--ease),top .55s var(--ease),width .55s var(--ease),
    height .55s var(--ease),background .4s}
.tabs.has-pill .tab.is-on{background:transparent;box-shadow:none;color:#fff}

/* (public-ux · strip) Compaction, so the wrapped rail spends as few rows as
   it can before the scroll breakpoint hands over. */
@media (max-width:1240px){
  .tabs__rail{gap:6px}
  .tab{padding:12px 14px;font-size:13.5px;gap:7px}
}

/* (public-ux · strip) SCROLL MODE — under 960 (see the note above).
   The fade is the affordance: a pill that runs under a soft edge reads as
   "there is more this way", where a pill that ends flush against the
   container reads as the end of the list.
   It is asymmetric ON PURPOSE. A symmetric both-edges mask fades the FIRST
   tab at scrollLeft 0, where nothing is hidden on that side, which is a lie
   in the other direction. The two stops are custom properties so site.js
   can set each from the rail's own measured rect — geometry, never a
   scrollLeft convention, so RTL is right by construction. The declared
   defaults below are the no-JS resting state: at the start of the list the
   hidden content is at the inline END, which is physically right in LTR and
   physically left in RTL — hence the one [dir] branch. */
@media (max-width:959px){
  .tabs{
    overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch;
    --tabs-fade-l:0px;--tabs-fade-r:30px;
    mask-image:linear-gradient(to right,transparent 0,#000 var(--tabs-fade-l),#000 calc(100% - var(--tabs-fade-r)),transparent 100%);
    -webkit-mask-image:linear-gradient(to right,transparent 0,#000 var(--tabs-fade-l),#000 calc(100% - var(--tabs-fade-r)),transparent 100%);
    /* The two stops flip instantly and are DELIBERATELY not transitioned.
       Easing them means registering both with @property, which makes them
       animated values — and an animated custom property that never finishes
       (measured: a transition stuck in `running`, computed value pinned at
       the start) leaves the mask at zero and the fade invisible. A 30px mask
       edge appearing at a scroll boundary needs no easing to read. */
  }
  [dir=rtl] .tabs{--tabs-fade-l:30px;--tabs-fade-r:0px}
  .tabs__rail{flex-wrap:nowrap;width:max-content;min-width:100%}
}

.pane{display:none}
.pane.is-on{display:block;animation:fadeUp .55s var(--ease)}
@keyframes fadeUp{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:none}}

.pane__head{display:grid;grid-template-columns:1.45fr 1fr;gap:clamp(26px,4vw,80px);
  padding-block-end:clamp(30px,3vw,50px);margin-block-end:clamp(30px,3vw,50px);
  border-block-end:1px solid var(--line)}
.pane__sum{color:var(--mut);line-height:1.9;margin-block-start:16px;font-size:1.06rem}
.facts{display:grid;grid-template-columns:repeat(4,1fr);align-self:start;
  border:1px solid var(--line);border-radius:20px;overflow:hidden;background:var(--bg2)}
.fact{padding:22px 14px;text-align:center;border-inline-end:1px solid var(--line)}
.fact:last-child{border-inline-end:0}
.fact b{display:block;font-family:var(--ff-m);font-weight:600;font-size:1.35rem;line-height:1.1;color:var(--navy)}
html[data-theme="dark"] .fact b{color:var(--sky)}
.fact span{font-size:12px;color:var(--mut);display:block;margin-block-start:7px}

/* 1.35fr, not 1.6fr: the aside is a two-column bento now, and at the old
   ratio each cell was ~240px — narrow enough to wrap every outcome onto four
   lines. Widening the sidebar buys ~25px per cell without crowding the
   syllabus column, which still gets the larger share. */
.cur{display:grid;grid-template-columns:1.35fr 1fr;gap:clamp(26px,3vw,60px);align-items:start}
.sub{font-family:var(--ff-m);font-size:12px;letter-spacing:.18em;color:var(--azure);
  margin-block-end:18px;display:block}
[lang="ar"] .sub{font-family:var(--ff-b);font-weight:700;letter-spacing:.03em;font-size:14px}

/* ── the syllabus as a traced timeline (#122) ─────────────────────────────
   (D-040) The white module cards are gone. The units are transparent
   editorial rows on a rail, and a gradient beam with a glowing head traces
   the reader's progress down that rail. `--beam` is written by traceModules()
   in site.js. Under reduced motion (and with no JS at all) no beam is injected
   and no node ever fills — `--beam` keeps its 0 default and `.mod.is-past` is
   never applied, so the rail reads as a plain static index. That is the
   intended calm fallback: an all-filled rail would imply "you have read all of
   this", which is not true at rest. */
.mods{display:grid;gap:10px;position:relative;padding-inline-start:40px}
.mods::before{content:'';position:absolute;inset-block:6px;inset-inline-start:5px;width:2px;
  background:var(--line);border-radius:2px}
.mods__beam{position:absolute;inset-block-start:6px;inset-inline-start:5px;width:2px;
  height:var(--beam,0);border-radius:2px;
  background:linear-gradient(180deg,transparent,var(--accent));pointer-events:none}
.mods__beam::after{content:'';position:absolute;inset-block-end:-4px;inset-inline-start:-3px;
  width:8px;height:8px;border-radius:50%;background:var(--accent);
  box-shadow:0 0 12px 3px color-mix(in srgb,var(--accent) 55%,transparent)}
/* (D-041 · V1) The flat hairline rows read as wireframe, so the units are
   soft elevated cards again — they just float BESIDE the traced rail now
   rather than replacing it. `border:0` on the row button below is
   load-bearing: a <button> inherits the UA's `2px outset` border, and
   .btn / .iconbtn each clear it by hand; .tab, .mod__b and .qa__b never did. */
.mod{position:relative;border-radius:18px;background:var(--bg2);
  box-shadow:inset 0 0 0 1px var(--line),var(--shadow);
  transition:translate .35s var(--ease),box-shadow .35s var(--ease)}
.mod:hover{translate:0 -2px;box-shadow:inset 0 0 0 1px var(--line2),var(--shadow2)}
.mod.is-open{box-shadow:inset 0 0 0 1px var(--line2),var(--shadow2)}
/* the accent hairline that caps an open card */
.mod::after{content:'';position:absolute;inset-block-start:0;inset-inline:14px;height:2px;
  border-radius:2px;background:linear-gradient(90deg,transparent,var(--accent) 45%,transparent);
  transform:scaleX(0);transform-origin:0 50%;transition:transform .45s var(--ease)}
[dir="rtl"] .mod::after{transform-origin:100% 50%}
.mod.is-open::after{transform:scaleX(1)}
/* the node dot lives on the rail, not on the card, so the hover lift above
   has to be cancelled here or the dot drifts off the beam it marks */
.mod::before{content:'';position:absolute;inset-block-start:24px;inset-inline-start:-40px;
  width:12px;height:12px;border-radius:50%;background:var(--bg);
  box-shadow:inset 0 0 0 2px var(--line2);
  transition:box-shadow .35s var(--ease),translate .35s var(--ease)}
.mod:hover::before{translate:0 2px}
.mod.is-past::before,.mod.is-open::before{box-shadow:inset 0 0 0 4px var(--accent)}
.mod__b{width:100%;display:flex;align-items:center;gap:14px;padding:16px 20px;background:transparent;
  border:0;border-radius:18px;cursor:pointer;text-align:start;transition:background .3s}
.mod.is-open .mod__b{background:color-mix(in srgb,var(--accent) 7%,transparent);
  border-end-start-radius:0;border-end-end-radius:0}
.mod__n{flex:0 0 auto;width:28px;height:28px;border-radius:50%;display:grid;place-items:center;
  font-family:var(--ff-m);font-size:12px;font-weight:600;
  background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent-lift)}
.mod__t{flex:1;font-weight:600;font-size:1rem;line-height:1.45}
.mod__h{font-family:var(--ff-m);font-size:11.5px;color:var(--mut);white-space:nowrap;
  padding:5px 12px;border-radius:999px;background:var(--bg3)}
.mod__ic{width:16px;height:16px;display:block;color:var(--mut);
  transition:transform .35s var(--ease);flex-shrink:0}
.mod.is-open .mod__ic{transform:rotate(180deg)}
.mod__p{max-height:0;overflow:hidden;transition:max-height .5s var(--ease)}
.mod__p ul{padding:4px 20px 22px 62px;display:grid;
  grid-template-columns:repeat(2,1fr);gap:11px 34px}
[dir="rtl"] .mod__p ul{padding-inline:62px 20px}
.mod__p li{list-style:none;position:relative;padding-inline-start:21px;font-size:.98rem;
  color:var(--mut);line-height:1.7}
.mod__p li::before{content:'';position:absolute;inset-inline-start:0;inset-block-start:12px;
  width:9px;height:2px;background:var(--accent);opacity:.85;border-radius:2px}

/* ── the aside as a bento with one shared light source (#106) ─────────────
   Six cells, one pointer position written on the grid and translated into
   each cell's own coordinates by bentoLight() in site.js, so the highlight
   reads as a single lamp moving over the whole panel rather than six
   independent hovers. Touch and reduced-motion never get `is-lit`, so the
   cells keep their plain hairline. */
.aside{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;
  position:sticky;inset-block-start:110px}
.box{position:relative;padding:clamp(20px,1.7vw,28px);border-radius:22px;background:var(--bg2);
  box-shadow:inset 0 0 0 1px var(--line)}
.box::before{content:'';position:absolute;inset:0;border-radius:inherit;padding:1px;
  background:radial-gradient(220px circle at var(--cx,50%) var(--cy,50%),
    var(--accent),transparent 62%);
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  mask-composite:exclude;opacity:0;transition:opacity .35s;pointer-events:none}
.aside.is-lit .box::before{opacity:.9}
/* (D-041 · V2) With the tools and requirements cells deleted the bento holds
   four: outcomes, projects, careers, CTA. Outcomes is by far the longest list
   and reads badly in a half-width cell, so it takes the full row; projects
   and careers pair off underneath; the CTA closes the panel full-width. That
   is a 1 / 2 / 1 rhythm with no odd cell left hanging.
   That rhythm is carried by explicit modifier classes, not by DOM position,
   so reordering or inserting a cell cannot silently move the full-width span
   onto the wrong one. */
.box--wide,.box--cta{grid-column:1/-1}
@media (max-width:560px){.aside{grid-template-columns:1fr}}
.box h4{font-family:var(--ff-m);font-size:12px;letter-spacing:.14em;color:var(--azure);
  margin-block-end:17px}
[lang="ar"] .box h4{font-family:var(--ff-b);font-weight:700;letter-spacing:.02em;font-size:1rem}
.box ul{display:grid;gap:12px}
.box li{list-style:none;display:flex;gap:11px;font-size:.98rem;color:var(--mut);line-height:1.65}
.box li .ico{width:16px;height:16px;flex-shrink:0;margin-block-start:5px;color:var(--accent-lift)}
.box--cta{background:linear-gradient(150deg,color-mix(in srgb,var(--accent) 13%,var(--bg2)),var(--bg2))}
.box--cta p{font-size:.98rem;color:var(--mut);margin-block:8px 18px}
.box--cta b{font-size:1.22rem}

/* ═══════════════════════════════════════════════════════════ journey ═══ */
.tl{position:relative;margin-block-start:clamp(36px,4vw,66px);
  display:grid;grid-template-columns:repeat(3,1fr);border-block-start:2px solid var(--line)}
.tl__p{position:absolute;inset-block-start:-2px;inset-inline-start:0;height:2px;width:0;opacity:0;
  background:linear-gradient(90deg,var(--navy),var(--orange));transition:opacity .3s}
/* (C6) a glowing tracer head at the leading edge of the progress line —
   timeline()'s onUpdate toggles the line's opacity so this head never
   flashes at 0 width before the visitor has scrolled into the section */
.tl__p::after{content:'';position:absolute;inset-inline-end:-5px;inset-block-start:50%;
  translate:0 -50%;width:10px;height:10px;border-radius:50%;background:var(--orange);
  box-shadow:0 0 14px 4px rgba(255,107,53,.5)}
.step{position:relative;padding:clamp(28px,2.5vw,46px) clamp(18px,1.8vw,36px);
  border-inline-end:1px solid var(--line);border-block-end:1px solid var(--line)}
.step:nth-child(3n+1){border-inline-start:1px solid var(--line)}
.step__d{position:absolute;inset-block-start:-9px;inset-inline-start:clamp(18px,1.8vw,36px);
  width:16px;height:16px;border-radius:50%;background:var(--bg);
  box-shadow:inset 0 0 0 2px var(--line2);transition:.4s var(--ease)}
.step.is-in .step__d{box-shadow:inset 0 0 0 5px var(--navy),0 0 0 5px rgba(0,66,141,.15)}
/* (C6) a one-shot pulse ring on the step dot the moment it activates —
   pure CSS off the same .is-in the existing box-shadow swap already uses */
.step__d::after{content:'';position:absolute;inset:-4px;border-radius:50%;
  border:2px solid var(--navy);opacity:0;transform:scale(.6)}
.step.is-in .step__d::after{animation:stepPulse .7s var(--ease) 1 both}
@keyframes stepPulse{0%{opacity:.7;transform:scale(.6)}100%{opacity:0;transform:scale(1.9)}}
.step__n{font-family:var(--ff-m);font-size:12px;letter-spacing:.2em;color:var(--azure);
  direction:ltr;unicode-bidi:isolate;display:inline-block}
.step h3{font-size:1.1rem;margin-block:11px 11px}
.step p{color:var(--mut);font-size:1rem;line-height:1.8}

/* ══════════════════════════════════ why — cursor spotlight ════════════ */
.why{position:relative;overflow:hidden;background:var(--bg2)}
.why__spot{position:absolute;inset:0;pointer-events:none;z-index:1;opacity:0;transition:opacity .5s;
  background:radial-gradient(420px circle at var(--mx,50%) var(--my,50%),
    rgba(0,66,141,.11),rgba(255,107,53,.055) 44%,transparent 70%)}
.why:hover .why__spot{opacity:1}
.why__grid{display:grid;grid-template-columns:repeat(3,1fr);margin-block-start:clamp(34px,3.6vw,60px);
  position:relative;z-index:2;border-block-start:1px solid var(--line)}
.wcell{position:relative;z-index:2;padding:clamp(30px,2.7vw,48px) clamp(20px,2vw,40px);
  border-inline-end:1px solid var(--line);border-block-end:1px solid var(--line);
  transition:background .4s}
.wcell:nth-child(3n){border-inline-end:0}
.wcell:hover{background:var(--bg)}
/* (C7) desktop-only: a single pill glides between cells on pointerenter
   instead of each cell swapping its own background — has-pill only lands
   when the why() IIFE actually runs (it bails on touch/reduced), so the
   plain per-cell hover above stays the fallback everywhere else */
.why__grid.has-pill{position:relative}
.why__pill{position:absolute;z-index:1;border-radius:18px;background:var(--bg);opacity:0;
  transition:left .45s var(--ease),top .45s var(--ease),width .45s var(--ease),
    height .45s var(--ease),opacity .3s;pointer-events:none}
.why__grid.has-pill .wcell:hover{background:transparent}
.wcell__i{width:50px;height:50px;border-radius:16px;display:grid;place-items:center;
  background:rgba(0,66,141,.09);color:var(--navy);margin-block-end:24px}
html[data-theme="dark"] .wcell__i{background:rgba(88,176,245,.15);color:var(--sky)}
.wcell__i .ico{width:24px;height:24px}
.wcell h3{font-size:1.1rem;margin-block-end:12px}
.wcell p{color:var(--mut);font-size:1rem;line-height:1.8}

/* ═════════════════════════════════════════════════════════════ games ═══ */
/* The three published games. The section carries .on-dark itself rather than
   inheriting the page theme: every render is a transparent PNG lit for a dark
   ground, and the games' own hub is dark, so a light-theme visitor does not
   get a white flash on the way out.
   The art sits on a radial pool, tinted per card through --pool, instead of a
   flat fill — each render is a single cut-out figure with no floor, and the
   pool gives it something to stand on without inventing a background it was
   never drawn against. The three tints are the ones the hub already uses. */
.gcards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  gap:clamp(14px,1.5vw,22px)}
.gcard{display:flex;flex-direction:column;overflow:hidden;border-radius:20px;
  background:var(--bg2);box-shadow:inset 0 0 0 1px var(--line);
  color:inherit;text-decoration:none;
  transition:transform .45s var(--ease),box-shadow .45s var(--ease)}
.gcard:hover{transform:translateY(-6px);
  box-shadow:inset 0 0 0 1.6px var(--sky),var(--shadow2)}
.gcard:focus-visible{outline:3px solid var(--sky);outline-offset:4px}
/* A square panel, not a landscape one: two of the three renders are portrait
   (600×900 and 640×900) and a 4/3 box wastes its width while starving them of
   the height they actually need. */
.gcard__art{position:relative;aspect-ratio:1/1;overflow:hidden;
  background:radial-gradient(ellipse at 50% 64%,var(--pool,#365453),#0a1418 72%)}
/* object-fit does the letterboxing, so the three different aspect ratios all
   sit at the same optical weight. A percentage HEIGHT here would not work: the
   panel's own height comes from aspect-ratio, so a percentage resolves against
   an indefinite box and the image overflowed its panel by 100px. Filling the
   padded box and letting object-fit scale inside it has no such dependency. */
.gcard__art img{position:absolute;inset:0;width:100%;height:100%;
  object-fit:contain;padding:9% 12%;box-sizing:border-box;
  filter:drop-shadow(0 14px 26px rgba(0,0,0,.55));
  transition:transform .5s var(--ease)}
.gcard:hover .gcard__art img{transform:scale(1.05)}
.gcard__body{display:flex;flex-direction:column;flex:1;
  padding:clamp(18px,1.7vw,28px)}
.gcard__genre{font-family:var(--ff-m);font-size:11px;font-weight:500;
  letter-spacing:.14em;text-transform:uppercase;color:var(--sky)}
/* Same contract .eyebrow, .hero__k and .foot h4 already keep: --ff-m is a Latin
   label face with no Arabic glyphs, so Arabic drops through to a system
   fallback and the .14em tracking prises the joined script apart letter by
   letter. Arabic gets the body face and near-zero tracking instead. The card
   title needs no rule here — it is an h3, and line 101 already covers those. */
[lang="ar"] .gcard__genre{font-family:var(--ff-b);font-weight:700;
  letter-spacing:.02em;font-size:12.5px;text-transform:none}
.gcard__t{font-size:1.12rem;line-height:1.55;margin-block:11px 10px}
.gcard__d{color:var(--mut);font-size:.95rem;line-height:1.85;margin:0 0 22px}
.gcard__play{margin-block-start:auto;display:flex;align-items:center;gap:9px;
  padding-block-start:16px;border-block-start:1px solid var(--line);
  font-family:var(--ff-m);font-weight:600;color:var(--sky)}
[lang="ar"] .gcard__play{font-family:var(--ff-b);font-weight:700}
/* same arrow contract as .btn: mirrored in RTL, and the hover nudge is applied
   on top of that mirror so it always travels the way the text reads */
.gcard__play .ico{width:16px;height:16px;transition:transform .4s var(--ease)}
[dir="rtl"] .gcard__play .ico{transform:scaleX(-1)}
.gcard:hover .gcard__play .ico{transform:translateX(5px)}
[dir="rtl"] .gcard:hover .gcard__play .ico{transform:scaleX(-1) translateX(5px)}

.gfoot{display:flex;align-items:center;justify-content:space-between;gap:24px;
  flex-wrap:wrap;margin-block-start:clamp(26px,2.6vw,42px);
  padding-block-start:clamp(22px,2.2vw,32px);border-block-start:1px solid var(--line)}
.gfoot p{margin:0;color:var(--mut);font-size:1rem;line-height:1.8;max-width:56ch}
.gfoot__acts{display:flex;align-items:center;gap:12px;flex-wrap:wrap}

@media(max-width:1000px){
  .gcards{grid-template-columns:1fr}
  /* one column: the card turns on its side so three of them still fit on a
     phone screen without three full-height art panels to scroll past */
  .gcard{display:grid;grid-template-columns:132px 1fr;align-items:stretch}
  .gcard__art{aspect-ratio:auto;height:100%;min-height:190px}
  .gcard__art img{padding:12% 8%}
  .gcard:hover{transform:none}
}
@media(max-width:560px){
  .gcard{grid-template-columns:110px 1fr}
  .gcard__d{font-size:.9rem;margin-block-end:16px}
  .gfoot__acts{width:100%}
  .gfoot__acts .btn{flex:1 1 100%}
}
@media(prefers-reduced-motion:reduce){
  .gcard,.gcard__art img,.gcard__play .ico{transition:none}
  .gcard:hover{transform:none}
  .gcard:hover .gcard__art img{transform:none}
}


/* ═══════════════════════════════════════════════════════════════ faq ═══ */
/* (D-041 · V3) The FAQ speaks the same card language as the syllabus units:
   one centred column of soft elevated cards, the index kept but demoted to a
   quiet mono numeral, and the plus icon carried in its own round chip. */
.faq{display:grid;gap:10px;max-width:920px;margin-inline:auto;
  margin-block-start:clamp(30px,3vw,50px)}
.qa{position:relative;align-self:start;border-radius:18px;background:var(--bg2);
  box-shadow:inset 0 0 0 1px var(--line),var(--shadow);
  transition:translate .35s var(--ease),box-shadow .35s var(--ease)}
.qa:hover{translate:0 -2px;box-shadow:inset 0 0 0 1px var(--line2),var(--shadow2)}
.qa.is-open{box-shadow:inset 0 0 0 1px var(--line2),var(--shadow2)}
/* the accent hairline that caps an open card — the same cue the syllabus
   cards use, so the two lists read as one system */
.qa::before{content:'';position:absolute;inset-block-start:0;inset-inline:16px;height:2px;
  border-radius:2px;background:linear-gradient(90deg,transparent,var(--navy) 45%,transparent);
  transform:scaleX(0);transform-origin:0 50%;transition:transform .45s var(--ease)}
[dir="rtl"] .qa::before{transform-origin:100% 50%}
html[data-theme="dark"] .qa::before{background:linear-gradient(90deg,transparent,var(--sky) 45%,transparent)}
.qa.is-open::before{transform:scaleX(1)}
.qa__b{width:100%;display:flex;align-items:center;gap:clamp(12px,1.4vw,20px);border:0;
  padding:clamp(20px,1.9vw,24px);background:transparent;border-radius:18px;
  cursor:pointer;text-align:start;font-weight:600;font-size:clamp(1.02rem,1.2vw,1.12rem);
  transition:color .25s}
.qa__i{font-family:var(--ff-m);font-size:12px;font-weight:600;letter-spacing:.12em;
  color:var(--azure);flex-shrink:0;opacity:.85}
.qa__t{flex:1;line-height:1.5}
.qa__ic{width:32px;height:32px;flex-shrink:0;border-radius:50%;display:grid;place-items:center;
  color:var(--navy);background:rgba(0,66,141,.09);
  transition:transform .35s var(--ease),background .3s}
.qa__ic .ico{width:15px;height:15px}
html[data-theme="dark"] .qa__ic{color:var(--sky);background:rgba(88,176,245,.15)}
.qa.is-open .qa__ic{transform:rotate(45deg)}
.qa__p{max-height:0;overflow:hidden;transition:max-height .5s var(--ease)}
/* (C8) the answer itself fades/rises in as the accordion opens — layered on
   top of the existing max-height mechanism, which is untouched. Once JS has
   split the paragraph into word units (#125) the whole-paragraph fade steps
   aside and the units carry the entrance instead. */
.qa__p p{padding:0 clamp(20px,1.9vw,24px) clamp(22px,2vw,28px);
  padding-inline-start:calc(clamp(20px,1.9vw,24px) + clamp(12px,1.4vw,20px) + 26px);
  color:var(--mut);font-size:1.03rem;line-height:1.9;
  opacity:0;translate:0 8px;transition:opacity .4s var(--ease) .1s,translate .4s var(--ease) .1s}
.qa.is-open .qa__p p{opacity:1;translate:0 0}
.qa__p.is-split p{opacity:1;translate:none;transition:none}

/* ═══════════════════════════════════════════════════════════ contact ═══ */
.contact__grid{display:grid;grid-template-columns:1.1fr .9fr;gap:clamp(30px,4vw,90px);align-items:start}
.form{display:grid;gap:13px;margin-block-start:28px}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:13px}
.field{position:relative}
.field label{position:absolute;inset-block-start:16px;inset-inline-start:19px;font-size:.98rem;
  color:var(--mut);pointer-events:none;transition:.3s var(--ease)}
.field input,.field select,.field textarea{width:100%;padding:26px 19px 12px;border-radius:16px;
  background:var(--bg2);border:0;box-shadow:inset 0 0 0 1px var(--line);font-size:1.02rem;
  transition:box-shadow .3s;outline:none}
.field textarea{min-height:136px;resize:vertical;padding-block-start:31px}
.field select{padding-block:23px 15px;padding-inline-end:48px;appearance:none;cursor:pointer;
  /* the OS keeps the option list; only the closed control is ours */
  -webkit-appearance:none;-moz-appearance:none}

/* (public-ux · selects) THE NATIVE CONTROL, STYLED — never rebuilt in JS.
   `appearance:none` above has always stripped the OS arrow and put nothing
   back, so both selects read as text inputs that mysteriously refuse to
   type. The chevron is a MASK painted with `currentColor`, not a coloured
   background-image: one declaration then serves light and dark, because the
   colour is a token that already flips. `inset-inline-end` places it on the
   correct side in Arabic with no [dir] branch.
   It is a pseudo-element of .field rather than a background on the select
   so it can be coloured by the select's own hover/focus state, and it is
   `pointer-events:none` so the whole control — chevron included — still
   opens the browser's own picker. On a phone that picker is the native
   wheel/sheet, which is the best one on the device; nothing here degrades
   it, and nothing here needs JavaScript to work.
   `:has()` rather than a class, so a select added to ANY .field on the
   public site is dressed automatically. Where :has() is missing the select
   is still perfectly usable — it just loses the chevron, which is exactly
   the state that shipped before this. */
.field:has(select){
  --chev:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4.5 8.75 7.5 7.5 7.5-7.5'/%3E%3C/svg%3E");
  /* the select's OWN content line box: font-size × line-height, the two
     values declared for it above. See the anchoring note below. */
  --sel-line:calc(1.02rem * 1.72);
}
/* Anchored from the TOP of .field by the select's own padding, not centred on
   .field — because the client validator prints its message inside .field, and
   a centred chevron would drift down the moment an error appeared. Offset by
   the select's padding-block-start and given the select's own line box, the
   chevron sits on the text line of the closed control and stays there however
   tall the field grows. */
.field:has(select)::after{
  content:'';position:absolute;pointer-events:none;
  inset-block-start:23px;inset-inline-end:20px;
  inline-size:13px;block-size:var(--sel-line);
  color:var(--mut);background:currentColor;
  -webkit-mask:var(--chev) center/13px 13px no-repeat;
  mask:var(--chev) center/13px 13px no-repeat;
  transition:color .3s var(--ease),translate .3s var(--ease);
}
.field select:hover{box-shadow:inset 0 0 0 1px var(--line2)}
.field:has(select:hover)::after{color:var(--ink);translate:0 1px}
.field:has(select:focus)::after{color:var(--focus)}
.field:has(select:disabled)::after{opacity:.4}
/* (apply-form-fields) The optional age input. A number input's spinner
   buttons are painted inside the field's own padding, right where the
   floating label's air is — and RTL does not move them consistently
   between engines, so the field's inline-end changed shape by browser.
   Dropped in both: the value is two digits on a numeric keypad, and the
   min/max range still constrains it exactly as before. */
.field input[type=number]{-moz-appearance:textfield;appearance:textfield}
.field input[type=number]::-webkit-outer-spin-button,
.field input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;appearance:none;margin:0}
.field input:focus,.field select:focus,.field textarea:focus{
  box-shadow:inset 0 0 0 2px var(--focus),0 0 0 4px var(--focus-ring)}
.field input:focus+label,.field input:not(:placeholder-shown)+label,
.field textarea:focus+label,.field textarea:not(:placeholder-shown)+label,
.field select:focus+label,.field select.has+label{
  inset-block-start:8px;font-size:11px;letter-spacing:.07em;color:var(--focus)}
/* (public-ux · selects) A select's resting state is now a REAL labelled
   prompt ("اختار مؤهلك الدراسي") instead of a blank highlighted row, so its
   floating label can never sit unfloated on top of that prompt — a select
   is never visually empty. The `.has` rule above still owns the *colour*,
   which is what now distinguishes answered from unanswered. */
.field select+label{inset-block-start:8px;font-size:11px;letter-spacing:.07em}
/* (C9) a native `invalid` submit shakes just the offending field instead of
   the whole form jumping — bound per-field in site.js, cleared on next input
   so the shake can replay on a second failed attempt */
@keyframes formShake{0%,100%{transform:translateX(0)}20%{transform:translateX(-7px)}
  40%{transform:translateX(6px)}60%{transform:translateX(-4px)}80%{transform:translateX(3px)}}
.field.is-shake{animation:formShake .45s cubic-bezier(.36,.07,.19,.97) 1}
.field.is-shake :is(input,select,textarea){box-shadow:inset 0 0 0 2px var(--orange-dk)}
/* (public-ux · validation) The shake is a 450ms event; `is-bad` is the STATE.
   It outlives the animation so the offending field stays marked until it is
   corrected — the shake alone vanished and left the visitor hunting for
   which of six fields the message belonged to. Declared after :focus so a
   field that is both focused and wrong still reads as wrong. */
/* (design-system) `:hover` is spelled out beside `:focus` rather than left to
   source order. `.field select:hover` and `.field input:focus` both repaint
   box-shadow from further up this file, and relying on "is-bad is declared
   later so it wins the tie" is a rule that breaks the first time someone
   moves a block — which is exactly how a hovered invalid field loses its red
   under the pointer, the one state the owner named out loud. */
.field.is-bad :is(input,select,textarea),
.field.is-bad :is(input,select,textarea):hover,
.field.is-bad :is(input,select,textarea):focus{
  box-shadow:inset 0 0 0 2px var(--orange-dk),0 0 0 4px rgba(204,74,18,.14)}
/* The label tint, in both of the two states a floating label can be in: the
   resting one (sitting over the field) and the raised one (`.is-up`, or
   raised by :focus/:not(:placeholder-shown)). Written as a bare descendant
   selector so it catches the label whichever rule is currently placing it. */
.field.is-bad label,.field.is-bad select+label{color:var(--orange-dk)}
.field.is-bad:has(select)::after,
.field.is-bad:has(select:hover)::after,
.field.is-bad:has(select:focus)::after{color:var(--orange-dk)}
.form__err{font-size:13.5px;color:var(--orange-dk);font-weight:600;
  animation:errIn .4s var(--ease) 1 both}
/* one message per field, sitting under the field it belongs to */
.field__err{display:block;font-size:12.8px;line-height:1.5;color:var(--orange-dk);
  font-weight:600;margin-block-start:7px;padding-inline-start:4px;
  animation:errIn .3s var(--ease) 1 both}
/* (design-system) SPECIFICITY, not decoration — and a real bug found in the
   browser walk. `.page__sec p` (0,2,1), declared further down this file for
   the standalone pages' body copy, outranks both `.field__err` (0,1,0) and
   `.form__err` (0,1,0). So on /contact every per-field message rendered in
   the muted grey of body text: a red-bordered field with a grey sentence
   under it, which is half a signal and reads as a hint rather than a refusal.
   `:is()` takes the specificity of its most specific argument and applies it
   to both branches, so this one selector is (0,3,0) either way and outranks
   the body-copy rule wherever in the file that rule happens to sit. */
.form :is(.form__err, .field .field__err){color:var(--orange-dk)}
.form[data-busy="1"] button[type=submit]{opacity:.62;cursor:progress}
@keyframes errIn{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
.ok,.page__ok{padding:17px 21px;border-radius:16px;font-size:1.02rem;font-weight:600;color:var(--navy);
  background:rgba(0,66,141,.08);box-shadow:inset 0 0 0 1px rgba(0,66,141,.26)}

/* ══════════════════════════════════════════════════ toasts (public) ═════
   (design-system) The public site had NO toast of any kind. A visitor who
   filled in the application form from the bottom of a 12-section page was
   redirected back to a green `.ok` panel that sits ABOVE the form — off
   screen, at the moment the browser restores the scroll position — so the
   only confirmation that anything had happened was a form that had gone
   blank. The panel stays (it is the durable record on the page, and it is
   what a visitor with JavaScript off sees); this is the transient half.

   Deliberately the platform toast's design, re-expressed in this
   stylesheet's own tokens rather than by importing lms-forms.css: same
   bottom-inline-end stack, same radius family, same 4px status bar on the
   reading-start edge, same draining progress line, same 5s timer paused on
   hover. What differs is the palette, because the two products have
   different ones — the public site has no green in its identity, and its
   whole success vocabulary is the navy `.ok` panel above, so a confirming
   toast is navy (sky on dark, via --focus) and a refusing one is
   --orange-dk, the colour every field error on this site already uses.

   `.site-` prefix, not `.lms-`: this file and lms-forms.css are never loaded
   into the same document, and a shared class name would invite exactly the
   cross-import the two surfaces have been kept apart to avoid. */
.site-toasts{
  position:fixed;
  /* MEASURED, not guessed: `.wa` — the floating WhatsApp button — is a
     58px circle pinned 24px from the bottom-END corner of every public
     page, which is the same corner a toast wants. 24+58+14 clears it, so
     the stack rises from just above the button instead of behind it. */
  inset-block-end:96px;
  /* logical, so the stack sits on the reading-END corner in both directions */
  inset-inline-end:20px;
  /* above `.wa` (150) as well as #nav and the mobile drawer: the two never
     overlap after the offset above, but a two-line toast on a short screen
     must not be able to slide under the button either. */
  z-index:160;
  display:flex;flex-direction:column;gap:10px;
  width:min(380px,calc(100vw - 40px));pointer-events:none}

.site-toast{
  pointer-events:auto;position:relative;overflow:hidden;
  display:flex;align-items:flex-start;gap:12px;
  padding:15px 17px;border-radius:18px;
  background:var(--bg2);color:var(--ink);
  border-inline-start:4px solid var(--line2);
  box-shadow:var(--shadow2),inset 0 0 0 1px var(--line);
  font-size:.97rem;line-height:1.6;
  animation:siteToastIn .24s var(--ease) both}

/* The auto-dismiss affordance. 5s linear, paused under :hover — the same two
   constants the raiser in site.js holds, so the bar tracks the real timer
   without either half having to learn about the other. Keep in step with
   TOAST_MS in public/assets/js/site.js. */
.site-toast::after{
  content:'';position:absolute;inset-block-end:0;inset-inline-start:0;
  height:2px;width:100%;background:currentColor;opacity:.32;
  /* transform-origin takes no logical keyword, hence the [dir] companion */
  transform-origin:left;animation:siteToastBar 5s linear forwards}
[dir="rtl"] .site-toast::after{transform-origin:right}
.site-toast:hover::after{animation-play-state:paused}

.site-toast--ok{border-inline-start-color:var(--focus);color:var(--focus)}
.site-toast--bad{border-inline-start-color:var(--orange-dk);color:var(--orange-dk)}
html[data-theme="dark"] .site-toast--bad{border-inline-start-color:var(--orange);color:var(--orange)}

/* The sentence itself stays ink, never the status colour: a two-line
   confirmation set in saturated navy reads as a heading, not as prose. */
.site-toast__msg{flex:1;color:var(--ink);font-weight:500;overflow-wrap:anywhere}

.site-toast__x{
  flex:none;border:0;background:transparent;color:inherit;opacity:.55;cursor:pointer;
  inline-size:24px;block-size:24px;display:grid;place-items:center;border-radius:8px;
  transition:opacity .18s var(--ease),background .18s var(--ease)}
.site-toast__x:hover{opacity:1;background:var(--bg3)}
.site-toast__x:focus-visible{opacity:1;outline:2px solid var(--focus);outline-offset:2px}
.site-toast__x svg{width:13px;height:13px}

.site-toast--out{animation:siteToastOut .24s var(--ease2) forwards}

@keyframes siteToastIn{from{opacity:0;transform:translateY(10px) scale(.98)}
  to{opacity:1;transform:none}}
@keyframes siteToastOut{from{opacity:1;transform:none}
  to{opacity:0;transform:translateY(10px) scale(.98)}}
@keyframes siteToastBar{from{transform:scaleX(1)}to{transform:scaleX(0)}}

/* Reduced motion: the toast still APPEARS and still dismisses itself, it just
   does not slide. The bar is hidden rather than frozen full-width, which
   would read as a permanent underline. */
@media (prefers-reduced-motion:reduce){
  .site-toast,.site-toast--out{animation:none}
  .site-toast::after{display:none}
}

@media (max-width:520px){
  /* Full width on a phone, still clear of the WhatsApp button — which does
     not shrink, so the vertical offset is the same 96px it is on desktop. */
  .site-toasts{inset-block-end:92px;inset-inline:12px;width:auto}
}

.cinfo{display:grid;grid-template-columns:repeat(2,1fr);gap:11px}
.ci{display:flex;gap:16px;padding:23px;border-radius:20px;background:var(--bg2);
  box-shadow:inset 0 0 0 1px var(--line);transition:box-shadow .3s,transform .4s var(--ease)}
.ci:hover{box-shadow:inset 0 0 0 1px var(--line2),var(--shadow);transform:translateY(-3px)}
.ci--wide{grid-column:1/-1}
.ci__i{width:46px;height:46px;border-radius:15px;display:grid;place-items:center;flex-shrink:0;
  background:rgba(0,66,141,.09);color:var(--navy)}
html[data-theme="dark"] .ci__i{background:rgba(88,176,245,.15);color:var(--sky)}
.ci__i .ico{width:21px;height:21px}
.ci b{display:block;font-size:1.02rem;margin-block-end:3px}
.ci span{font-size:.94rem;color:var(--mut);word-break:break-word}

/* ════════════════════════════════════════════════════════════ footer ═══ */
/* (D-056) Obsidian ground with a circuit-glow top edge: a hairline that runs
   bright at the centre and fades at both ends, sitting on a soft blue bloom —
   the same "current running through the seam" the cover artwork uses, drawn in
   CSS so it costs no image and mirrors itself for free in RTL. */
.foot{position:relative;overflow:hidden;background:#04070e;
  border-block-start:1px solid rgba(234,242,252,.10)}
.foot::before{content:'';position:absolute;inset-block-start:0;inset-inline:0;height:150px;
  pointer-events:none;z-index:0;
  background:
    linear-gradient(90deg,transparent,rgba(88,176,245,.16) 14%,rgba(88,176,245,.9) 50%,
      rgba(88,176,245,.16) 86%,transparent) top center/100% 1px no-repeat,
    radial-gradient(130% 100% at 50% 0,rgba(20,102,224,.20),transparent 68%)}
/* The glass mark, signing the corner. `overflow:hidden` on the footer is what
   crops it, so it reads as a watermark bleeding off the edge rather than a
   second logo dropped into the layout. */
.foot__wm{position:absolute;z-index:0;pointer-events:none;
  inset-block-end:-6%;inset-inline-end:-3%;width:min(30vw,430px);aspect-ratio:424/523;
  background-size:contain;background-repeat:no-repeat;background-position:center;
  opacity:.05;filter:drop-shadow(0 0 60px rgba(88,176,245,.4))}
@media (max-width:1100px){.foot__wm{display:none}}
/* (r2 review) #contact is deliberately tucked 28px over the footer's top
   edge for the curtain-edge read — but the first thing under that edge is
   this marquee, whose 20px padding left the tucked white card slicing the
   lettering in half. The tuck stays; the marquee just starts below it. The
   value tracks #contact's negative margin, including its ≤900px variant. */
.foot__mq{overflow:hidden;padding-block:calc(20px + 28px) 20px;
  border-block-end:1px solid rgba(234,242,252,.12);direction:ltr}
@media (max-width:900px){.foot__mq{padding-block-start:calc(20px + 16px)}}
.foot__mt{display:flex;gap:46px;align-items:center;width:max-content;direction:ltr;will-change:transform}
.foot__mt span{font-family:var(--ff-m);font-weight:500;font-size:12px;text-transform:uppercase;
  line-height:1;color:var(--mut);white-space:nowrap;letter-spacing:.28em}
/* The brand block is a real first COLUMN, sized by minmax so the four link
   lists keep a readable floor instead of being squeezed by a greedy fr. It
   only spans the row at the narrow breakpoint below, where it is laid out to
   fill that row rather than hug one end. `position:relative` lifts the content
   above the watermark and the glow edge. */
.foot__in{position:relative;z-index:1;display:grid;
  grid-template-columns:minmax(270px,1.5fr) repeat(4,minmax(150px,1fr));
  gap:clamp(26px,2.6vw,52px);
  max-width:var(--maxw);margin-inline:auto;padding:clamp(46px,4.4vw,76px) var(--pad)}
.foot__brand{display:flex;flex-direction:column;gap:24px;align-items:flex-start}
.foot h4{font-family:var(--ff-m);font-size:12px;letter-spacing:.18em;color:var(--sky);
  margin-block-end:19px}
[lang="ar"] .foot h4{font-family:var(--ff-b);font-weight:700;letter-spacing:.02em;font-size:1rem}
.foot ul{display:grid;gap:11px}
.foot li{list-style:none}
.foot a{font-size:.97rem;color:var(--mut);transition:color .25s}
.foot a:hover{color:var(--ink)}
.foot__brand p{font-size:.99rem;color:var(--mut);line-height:1.85;max-width:40ch;margin-block-start:20px}
/* Social buttons as real identity chips: a glass tile with a blue hairline
   that fills toward the accent on hover, matching the hero's stat chips rather
   than being four bare outlines. */
.foot__soc{display:flex;gap:11px;flex-shrink:0}
.foot__soc a{width:44px;height:44px;border-radius:13px;display:grid;place-items:center;
  color:rgba(234,242,252,.72);background:rgba(234,242,252,.04);
  box-shadow:inset 0 0 0 1px rgba(88,176,245,.22);
  transition:color .3s var(--ease),background .3s var(--ease),box-shadow .3s var(--ease)}
.foot__soc a:hover{color:#fff;background:rgba(88,176,245,.18);
  box-shadow:inset 0 0 0 1px var(--sky),0 8px 26px rgba(20,102,224,.3)}
.foot__soc .ico{width:18px;height:18px}
.foot__bot{position:relative;z-index:1;
  border-block-start:1px solid rgba(234,242,252,.12);font-size:13px;color:var(--mut)}
.foot__bot div{max-width:var(--maxw);margin-inline:auto;padding:21px var(--pad);
  display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap}

.wa{position:fixed;inset-block-end:24px;inset-inline-end:24px;z-index:150;width:58px;height:58px;
  border-radius:50%;display:grid;place-items:center;background:#25d366;color:#04240f;
  box-shadow:0 12px 34px rgba(37,211,102,.42);transition:transform .4s var(--ease)}
.wa:hover{transform:scale(1.08)}
.wa .ico{width:28px;height:28px}
/* (final walk D6) The FAB outranks the mobile drawer (z 150 vs 125), so an
   open drawer had a green bubble parked on its last row. The drawer already
   locks the body; hide the FAB for exactly as long as that lock is on. */
body.is-locked .wa{display:none}
/* (final walk D3) The hero stats hug the inline-end edge, and below ~1600px
   the last one lands inside the FAB's fixed 58px+24px corner — the figure
   hit-tested to `a.wa`, so the number was covered and the FAB unclickable
   through it. Above 1600px the row is wide enough to clear on its own.
   (anubi) KEPT, though the two-column hero already puts the chips inside the
   copy column and half a screen away from that corner. The clearance is a
   guarantee, not a patch: the column that holds them is the one that grows
   when the sanctum stacks away at 900px, and the failure it prevents is an
   unclickable FAB. HomeMotionContractTest pins it for exactly that reason. */
@media (max-width:1599px){
  .hero__stats{padding-inline-end:72px}
}

/* (final walk D9) A Latin run inside an RTL paragraph loses its trailing
   punctuation to the bidi algorithm — "© 2026 ANUPET Academy." rendered as
   ".ANUPET Academy 2026 ©". Isolating the run gives it its own embedding
   level, and inline-block keeps it from breaking across lines mid-run.
   Same intent as app.css's `.nowrap-data`, minus the nowrap, restated here
   because the marketing shell loads site.css alone. */
/* (owner · course page) `max-width` is new, and it is what makes this class
   safe to put around admin-authored copy rather than one hand-written footer
   line. `display:inline-block` is the half of this rule that does the work on a
   course page — a Latin phrase inside Arabic ("(Freelance Designer)") splits
   across a line break and comes back with its brackets mirrored onto the wrong
   sides, and an inline-block cannot be broken into — but an unbroken box is
   also a box that overflows a 288px card if the phrase is longer than one.
   Capped, a long run wraps inside its own column instead of running out of it,
   and the short ones that are the whole problem still never break at all.
   Measured on /courses/graphic-design with every Latin run wrapped in this
   class: runs split across lines went 3 -> 0 at 375 and 1 -> 0 at 1280. The
   wrapping itself is not something a stylesheet can do — see the report. */
.ltr-run{direction:ltr;unicode-bidi:isolate;display:inline-block;max-width:100%}

/* (responsive sweep R7) The footer motto ends with a Latin company name and
   a full stop the blade appends. To the bidi algorithm that stop is a
   PARAGRAPH-FINAL NEUTRAL: it takes the paragraph's own RTL direction and
   lands at the far left of the line, so a motto that wraps mid-name reads
   ".Programmer". `embed`, not `isolate`, and the difference is the whole
   fix: an isolate is opaque to the outer algorithm and would still be
   placed as a neutral, exactly where it is now. Embedding puts the stop on
   the same level as the Latin run beside it, so the two resolve as one LTR
   run and the stop stays where a reader expects it.
   Scoped to the stop alone because the Latin NAME lives inside a
   super-editable setting (site.parent) and cannot be wrapped from a view. */
.ltr-stop{direction:ltr;unicode-bidi:embed}

/* ═══════════════════════════════════════════════════════ scroll reveal ═ */
.rv{opacity:0;transform:translateY(30px)}
.rv.is-in{opacity:1;transform:none;transition:opacity .85s var(--ease),transform .85s var(--ease)}
.rv-d1.is-in{transition-delay:.08s}.rv-d2.is-in{transition-delay:.16s}
.rv-d3.is-in{transition-delay:.24s}.rv-d4.is-in{transition-delay:.32s}

/* ════════════════════════════════════════════════════════ responsive ═══ */
@media (max-width:1500px){
  .values{grid-template-columns:repeat(3,1fr)}
  .val:nth-child(3n){border-inline-end:0}
  .val:nth-child(-n+3){border-block-end:1px solid var(--line)}
  /* (D-056) The brand block used to be pushed onto its own full-width row from
     1500px down — the single most common desktop width included — where its
     content filled ~500px of ~1336 and the rest was the dead void the redesign
     was called for. It stays a column here; the minmax floors on .foot__in keep
     the four link lists readable at this width without it. */
}
@media (max-width:1240px){
  .menu{display:none}
  .burger{display:flex}
  /* the centred .menu (margin-inline:auto) was the pill's spacer — with it
     hidden, the controls hugged the brand and left a dead void across the
     pill (user-reported at ~940px). Re-anchor: brand at inline-start,
     controls at inline-end, like every collapsed navbar. */
  .nav__cta{margin-inline-start:auto}
  .about__top,.pane__head,.cur,.contact__grid{grid-template-columns:1fr}
  /* (responsive sweep R2) Collapsing .contact__grid to one column left the
     form with no width cap at all: the submit button measured 1151px at
     1240. A form reads as a form only while its fields stay near text
     measure. Anchored at the inline-start (margin-inline-end:auto) rather
     than centred, so it lines up with the heading above it in both
     directions instead of drifting away from the reading edge. */
  .contact__grid > :first-child{max-width:640px;margin-inline-end:auto}
  /* .ci--wide's full-row span was sized for the 511px side column. Once the
     card grid is page-wide those two cards become 1100px slabs among 550px
     neighbours — the ragged 2/1/1/2 pattern the sweep measured. Six equal
     cards in two columns is three even rows. */
  .ci--wide{grid-column:auto}
  .aside{position:static}
  .tl{grid-template-columns:repeat(2,1fr)}
  .step:nth-child(3n+1){border-inline-start:0}
  .step:nth-child(2n+1){border-inline-start:1px solid var(--line)}
  .stats{grid-template-columns:repeat(3,1fr)}
  .stat:nth-child(3n){border-inline-end:0}
  .stat:nth-child(-n+3){border-block-end:1px solid var(--line)}
  .why__grid{grid-template-columns:repeat(2,1fr)}
  .wcell:nth-child(3n){border-inline-end:1px solid var(--line)}
  .wcell:nth-child(2n){border-inline-end:0}
  .hero__stats{margin-inline-start:0;width:100%}
}
@media (max-width:900px){
  body{font-size:16px}
  .topbar .hide-sm{display:none}
  .nav__cta .btn{display:none}
  /* (D-046) the account menu collapses with the CTAs it replaced — below
     this width the drawer is the only menu, and it carries the same three
     items rather than a dropdown inside a dropdown */
  .nav__cta .uchip{display:none}
  .nav__in{padding-inline-start:10px}
  /* (owner · phone) What is left in the bar below this width is five icon
     buttons and the lockup, and at 375 every one of those buttons measured
     34x34 on a 5px gap — under the 44px touch floor, and tight enough to read
     as one clump rather than five controls.
     Growing the BOXES to 44 is the fix that does not fit. Measured intrinsic
     width of the row at 44px boxes: 28px of bar padding + 106.7 lockup + 8
     lockup gap + 220 of buttons = 362.7 before a single gap between them, so
     the gap would have to come down to 3px and five 44px targets would tile
     with no separation at all. The boxes keep their 34px and the HIT area is
     grown with a pseudo-element instead — the same trick `.topbar__soc a`
     already uses below — and the gap opens to exactly 10px, because 34 + 10
     makes the 44px areas meet edge to edge: touching, never overlapping,
     since an edge tap that fires the wrong control is worse than a small one.
     Measured after: 359px of 375, one line, five 44px targets.
     A symmetric negative `inset` rather than the 50%/50% + translate centring
     `.topbar__soc a` was written with: `inset-inline-start:50%` resolves to
     `right:50%` under dir="rtl" while `translate:-50%` stays physical and still
     pulls LEFT, so that construction lands the hit box a full width off target
     — measured, the burger's would have sat at x -31..13, off the screen.
     `inset:-5px` needs no axis at all: it grows every button by the half-gap on
     all four sides, which is 44px however wide the button under it is. */
  .nav__cta{gap:10px}
  .iconbtn{position:relative}
  .iconbtn::after{content:'';position:absolute;inset:-5px;min-width:44px;min-height:44px}
  .vm,.row2,.cinfo{grid-template-columns:1fr}
  .facts{grid-template-columns:repeat(2,1fr)}
  .fact:nth-child(2n){border-inline-end:0}
  .fact:nth-child(-n+2){border-block-end:1px solid var(--line)}
  .mod__p ul{grid-template-columns:1fr;padding-inline-start:46px}
  /* (D-056) Below 900px the brand block finally does span the row — and here
     it is laid out to FILL it: identity at the reading edge, social chips at
     the far one, which is the arrangement that has no void in it. */
  .foot__in{grid-template-columns:1fr 1fr}
  .foot__brand{grid-column:1/-1;flex-direction:row;align-items:center;
    justify-content:space-between;gap:28px;flex-wrap:wrap;
    padding-block-end:34px;border-block-end:1px solid rgba(234,242,252,.10)}
  .foot__brand-id{min-width:0}
  .tl,.values,.why__grid{grid-template-columns:1fr}
  .step,.val,.wcell{border-inline:0!important;border-block-end:1px solid var(--line)}
  .stats{grid-template-columns:repeat(2,1fr)}
  .stat:nth-child(2n){border-inline-end:0}
  /* (responsive sweep R3) The ≤1240 block's `:nth-child(-n+3)` row divider
     was written for THREE columns. In two, five stats lay out as [2,2,1] —
     so item 3 opened row 2 still carrying a divider that now ran under half
     a row, and item 5 sat alone beside an empty cell. Restated for two
     columns: everything above the last row gets the divider, and a lone
     final item takes the whole width instead of leaving a gap. */
  .stat:nth-child(-n+4){border-block-end:1px solid var(--line)}
  .stat:nth-child(odd):last-child{grid-column:1/-1}
}
/* (owner · phone) The floating mascot is `position:fixed` and stands ON the
   bottom edge of the viewport, so on a phone it came down on the
   24 / 45 / 256 / 05 capsules: measured at 375x812, the stat row ended at
   y 707.2 while the standing figure's head was already at y 679.5 — 27.7px of
   overlap. companion.js decides where the mascot stands; what the hero owes it
   is a lane to stand in.
   The reserve is the mascot's own geometry rather than a guessed number:
   companion.js draws the figure `bodyH` 300 + `base` 28 = 328 source pixels
   tall at petScale() = min(1, vw/520) x .56 up to 700px and x .66 above, so the
   lane is 328 x .56 x vw/520 = 35.3vw on a phone and never taller than
   328 x .66 = 216px anywhere inside this breakpoint. Change petScale() and this
   number has to move with it, or the mascot is back on the capsules.
   `min-height:700px` is the other half of the fix and it is a measured floor,
   not caution: below it the hero copy ALREADY overflows the viewport (375x667
   measured copy 486px + 40px of padding inside 567px of hero, with the stat row
   at y 632.9 and the figure's head at 534.5). Reserving a lane there cannot
   lift anything — the content is pinned under the header by padding-block-start
   — it only makes the hero 119px taller and pushes the next section down for
   nothing. Above the floor the lane lands inside the height the hero already
   had, and it does so at every width, which is why there is no max-width on
   this query: the mascot stands in the same corner on a desktop and the stats
   were under it there too. Measured before/after with the hero's own height
   unchanged in every case — 375x812 -27.7 -> +38.6, 1024x768 -71.9 -> +18.1,
   1280x800 -62.9 -> +45.1, 1440x900 -21.4 -> +68.6.
   `:has` gates the whole thing on the mascot actually being there — the bar's
   toggle sets `hidden` on the host, and config('companion.enabled') can drop it
   from the page entirely; neither case should leave a dead 130px band under the
   hero. Where `:has` is unsupported the rule is dropped whole, which is exactly
   the behaviour this replaces. */
@media (min-height:700px){
  body:has(#anupet-companion:not([hidden])) .hero__in{
    padding-block-end:calc(clamp(28px,4vh,54px) + min(216px,35.3vw))}
}
@media (max-width:560px){
  .grain{display:none}
  .hero__stats{gap:8px}
  .foot__in,.cinfo{grid-template-columns:1fr}
  /* On a phone the brand block stacks again — a 4-chip social row and a motto
     side by side inside 375px would wrap into something worse than a stack. */
  .foot__brand{flex-direction:column;align-items:flex-start}
  /* (responsive sweep R5) The top-strip social icons are 14x14 — far below
     any tap minimum. The HIT area is grown with a pseudo-element so the icons
     keep their compact size and the strip keeps its height, and
     the gap is opened so the targets sit edge to edge rather than overlapping
     (overlapping targets mean an edge tap opens the wrong network, which is
     worse than a small one).
     (owner · phone) Two things were wrong with it. 40 is still four pixels
     under the floor every other target in this pass was measured against — so
     44, and the gap follows it to 30 for the same edge-to-edge reason, 14 + 30
     being the new pitch; the strip holds three links, so the row grew by 12px
     of a 375px viewport. And the centring never worked in the direction this
     site actually ships in: `inset-inline-start:50%` resolves to `right:50%`
     under dir="rtl" while `translate:-50%` stays physical and still pulls left,
     so the box landed a full width to the OUTSIDE of the icon it was meant to
     enlarge. A symmetric negative `inset` has no axis to get wrong. */
  .topbar__soc{gap:30px}
  .topbar__soc a{position:relative}
  .topbar__soc a::after{content:'';position:absolute;inset:-15px;min-width:44px;min-height:44px}
}
/* (owner · phone) The inner-page bar — lockup, "رجوع للموقع", then the
   cat / games / theme / language cluster — measures 437.5px of intrinsic width
   against 375, so `.page__top-in`'s `flex-wrap` did what it was told and
   dropped the language button onto a row of its own. (That figure explains WHY
   the bar wraps on a phone; it is not where it stops wrapping — see the swept
   breakpoint below.) That wrap rule's own
   comment (see `.page__top-in`, D-046) says a guest's row still fits on one
   line; it stopped being true when the games icon joined the cluster, and the
   owner photographed the result.
   The back link is what goes, because it is the only control in that bar with
   a duplicate: its href is route('home'), the same destination as the lockup
   two elements away, which carries aria-label="ANUPET Academy" and is the link
   every site on the web puts home behind. Measured after: 335px of 375, one
   row.
   (review) …and the query is 452, not the 560 this first shipped under, nor the
   440 that replaced it. Both earlier numbers were reasoned from the 437.5px
   intrinsic figure above rather than swept, and the wrap does not happen there.
   Swept properly — hide rule forced off, one pixel at a time, identical on
   /contact, /privacy, /terms and /blog — THE ANSWER DEPENDS ON THE SCROLLBAR,
   and by exactly the width of one:
     · with a classic scrollbar (a desktop window dragged narrow, a small
       tablet): two rows up to 452, one row from 453.
     · with overlay scrollbars (every phone): two rows up to 437, one row
       from 438.
   452 − 437 = 15, which is the gutter — and this stylesheet is the reason it is
   always there: `scrollbar-gutter:stable` on `html` (see the note beside it)
   reserves it in both the locked and unlocked state, so a 452px window lays
   this bar out in 437px of space. The two sweeps are one measurement.
   A single `max-width` has to serve both, and 452 is the safe side of that
   choice. It never leaves the link standing on a two-row bar, which is the
   failure that costs a visitor a row of header on every inner page; the only
   thing it gives up is the link on a TOUCH device between 438 and 452, and no
   phone is that wide — 430 is the widest in common use. 437 would have had it
   the other way round: correct on a phone, and a two-row bar on every narrow
   desktop window from 438 to 452.
   The link is what goes rather than anything else in that bar because it is the
   only control there with a duplicate — the `.brand` lockup is the same href,
   though it carries no visible label. */
@media (max-width:452px){
  .page__back{display:none}
}
/* (owner · phone) 320px — the first SE, a Fold's cover screen — is the one
   width where this bar was not merely cramped. `.brand` and `.nav__cta` are
   both `flex-shrink:0`, so the row simply overflowed and `html{overflow-x:clip}`
   swallowed the evidence: measured `.nav__cta` at x -1, which is the burger's
   leading edge cut off the side of the screen, and a menu button is not an
   optional control. The wordmark folds into the mark it sits beside rather
   than the row losing a button — `.brand` keeps its aria-label, so the link's
   name is unchanged for anyone not reading it with their eyes. Measured after:
   282.3px of 320. */
@media (max-width:360px){
  .brand__txt{display:none}
}
@media (max-width:480px){
  /* (responsive sweep R6) Footer link anchors were 19px tall on a ~38px row
     pitch — half of every row looked tappable and was not. The padding makes
     the whole row the target; the list gap comes off by the same amount so
     the footer does not grow to pay for it. */
  .foot__in ul{gap:0}
  .foot__in li a{display:block;padding-block:10px}
  /* (owner · phone) The standalone-page footer is the same list of links in a
     different shell, and R6 above never reached it: measured on a course page
     at 375, `.page__foot-links a` is a 45.4x19 target on a 29px row pitch — the
     exact failure R6 was written for. Same treatment and the same trade: 13px
     of padding takes the row to 45, and the list gap comes off to pay for most
     of it. */
  .page__foot-links{gap:0}
  .page__foot-links a{display:block;padding-block:13px}
}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms!important;transition-duration:.001ms!important;
    animation-iteration-count:1!important}
  .rv{opacity:1;transform:none}
  .sw > span{transform:none}
  /* whole-section clip-path entrances and the footer curtain are a bigger
     bet than a small .rv fade — force them fully visible outright rather
     than trust a near-zero transition to still look calm */
  html.js-motion .sect,html.js-motion .foot{clip-path:none!important;opacity:1!important;transform:none!important}
  /* (D-054) The headline sweep stops outright rather than being sped to a
     near-zero duration: a clipped gradient frozen mid-travel would leave the
     line half-tinted. Painting it flat white is the resting state it should
     have had all along for a visitor who asked for no motion. */
  .shiny .rc{background-image:none!important;animation:none!important;
    color:#fff!important;-webkit-text-fill-color:#fff!important}
}
@media print{
  /* (D-052) .iband and .bfilm join the list for the same reason .hero already
     had: they are wordless full-bleed brand art on a near-black field, which
     on paper is a wasted sheet of toner and no information. */
  .nav,.topbar,.drawer,.wa,.grain,.progress,.hero,.reveal,.foot__mq,
  .iband,.bfilm{display:none!important}
  /* (r1 review) a section parked mid-entrance (clip-path/opacity/transform)
     never printed a clean page — force every section and the footer to
     their resting state regardless of scroll-triggered state */
  .sect,.foot{opacity:1!important;clip-path:none!important;transform:none!important}
}


/* ══════════════════════════════════════════ the Benha papyrus (بردية بنها) ═
   A full-bleed papyrus scroll carrying the campaign line. It keeps its own
   fixed palette — paper is its identity, so the dark toggle must not wash it.
   The hieroglyph rails and the circuit-ankh echo the brand's pharaonic keys. */
.papyrus{position:relative;isolation:isolate;margin-block:34px;
  background:linear-gradient(178deg,#efe3c6 0%,#ead9b4 46%,#e2cfa6 100%);
  color:#2b2316;padding-block:clamp(64px,6.4vw,104px)}
.papyrus::before,.papyrus::after{content:'';position:absolute;inset-inline:0;height:30px;z-index:3;
  background:linear-gradient(180deg,#caa96f,#8f6f3f 55%,#5f4626);
  border-radius:15px;box-shadow:0 8px 22px rgba(43,35,22,.35)}
.papyrus::before{inset-block-start:-15px}
.papyrus::after{inset-block-end:-15px;background:linear-gradient(0deg,#caa96f,#8f6f3f 55%,#5f4626)}
.papyrus__fiber{position:absolute;inset:0;z-index:-1;opacity:.5;mix-blend-mode:multiply;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='p'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.012 .18' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 .45 0 0 0 0 .38 0 0 0 0 .24 0 0 0 .35 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23p)'/%3E%3C/svg%3E")}
.papyrus__rail{position:absolute;inset-block:34px;width:60px;z-index:1;opacity:.5;
  color:#123e77;pointer-events:none}
.papyrus__rail--s{inset-inline-start:clamp(8px,2vw,34px)}
.papyrus__rail--e{inset-inline-end:clamp(8px,2vw,34px)}
.papyrus__rail svg{width:100%;height:100%}
.papyrus__in{position:relative;z-index:2;max-width:1240px;margin-inline:auto;
  padding-inline:clamp(84px,8vw,130px);display:grid;
  grid-template-columns:1.25fr .75fr;gap:clamp(26px,4vw,64px);align-items:center}

/* Arabic display voice — beIN only lives here */
.pap-ar{font-family:'beIN','IBM Plex Sans Arabic',sans-serif;font-weight:400}
.papyrus__eyebrow{display:inline-flex;align-items:center;gap:10px;
  font-family:var(--ff-m);font-weight:500;font-size:11.5px;letter-spacing:.22em;
  text-transform:uppercase;color:#8a5a20}
.papyrus__eyebrow .ankh-s{font-size:15px;letter-spacing:0}
.papyrus h2{font-size:clamp(1.9rem,3.6vw,3.3rem);line-height:1.42;color:#12294e;
  margin-block:18px 8px;letter-spacing:0}
/* (owner, 2026-09-08) The stroke is a real underline now, not an absolutely
   positioned ::after. An inline element's abs-positioned child is placed against
   that element's own box, and an Arabic face's box runs far below the letters it
   draws: measured here, the <em> ended 13px below its own <h2> and 5px INSIDE the
   paragraph beneath it, so every offset that looked right against the glyphs put
   the rule through the next line of copy. text-underline-offset is measured from
   the BASELINE instead, which is where a reader sees the word sitting, so the
   stroke cannot leave the word however deep the font's descent goes. */
.papyrus h2 em{font-style:normal;color:#c8502a;white-space:nowrap;
  text-decoration:underline;text-decoration-color:rgba(200,80,42,.5);
  text-decoration-thickness:3px;text-underline-offset:.16em;
  /* the stroke stays whole through descenders rather than breaking around them */
  text-decoration-skip-ink:none}
[dir="ltr"] .papyrus h2{line-height:1.16;font-family:var(--ff-d);font-weight:600;
  letter-spacing:-.02em}
.papyrus__sub{color:#5c4c30;font-size:clamp(1rem,1.1vw,1.14rem);line-height:1.9;max-width:56ch;
  margin-block-end:26px}

/* the three ink labels — اتعلم بجد · طبّق بإيدك · ابنِ شغلك */
.inkrow{display:flex;flex-wrap:wrap;gap:12px;margin-block-end:30px}
.ink{display:inline-block;padding:9px 22px 12px;background:#123e77;color:#f2e8cf;
  font-size:clamp(1.15rem,1.7vw,1.6rem);border-radius:4px;
  box-shadow:2px 3px 0 rgba(18,41,79,.22);transform:rotate(-1.6deg)}
.ink:nth-child(2){transform:rotate(1.2deg);background:#0d2c55}
.ink:nth-child(3){transform:rotate(-.8deg);background:#c8502a;color:#fdf3dd;
  box-shadow:2px 3px 0 rgba(140,52,22,.28)}
[dir="ltr"] .ink{font-family:var(--ff-d);font-weight:600;font-size:clamp(1rem,1.3vw,1.2rem)}

/* the round ink stamp — BUILT IN BENHA */
.stampwrap{display:flex;align-items:center;gap:26px;flex-wrap:wrap}
.stamp{width:132px;height:132px;border-radius:50%;flex-shrink:0;position:relative;
  display:grid;place-items:center;text-align:center;color:#123e77;
  border:2.5px solid currentColor;transform:rotate(-9deg);opacity:.88}
.stamp::before{content:'';position:absolute;inset:7px;border:1.2px solid currentColor;
  border-radius:50%;opacity:.8}
.stamp b{display:block;font-family:var(--ff-x);font-size:10px;letter-spacing:.2em;line-height:1.7}
.stamp .city{font-family:'beIN','IBM Plex Sans Arabic',sans-serif;font-weight:400;
  font-size:34px;line-height:1.05;display:block;margin-block:2px}
.stamp small{display:block;font-family:var(--ff-m);font-size:8px;letter-spacing:.24em;color:#8a5a20}
.stampwrap p{color:#5c4c30;font-size:.95rem;line-height:1.85;max-width:40ch;margin:0}

/* the circuit ankh — the pharaonic key, wired */
.ankh{width:min(240px,60%);margin-inline:auto;color:#123e77;
  filter:drop-shadow(0 10px 24px rgba(18,41,79,.18))}
.ankh svg{width:100%;height:auto}
.ankh .node{fill:#c8502a}

/* the riddle cartouche — لغز البردية */
.cartouche{grid-column:1/-1;margin-block-start:clamp(18px,2.4vw,34px);position:relative;
  border:2px solid #123e77;border-radius:60px;padding:clamp(20px,2.4vw,30px) clamp(22px,3vw,44px);
  display:flex;align-items:center;gap:clamp(16px,2.6vw,38px);flex-wrap:wrap;
  background:rgba(255,252,240,.45)}
.cartouche::after{content:'';position:absolute;inset-block:14px;inset-inline-end:-12px;width:4px;
  background:#123e77;border-radius:3px}
[dir="ltr"] .cartouche::after{inset-inline-end:auto;inset-inline-start:-12px}
.cartouche__t b{display:block;font-size:1.2rem;color:#12294e}
.cartouche__t span{font-size:.92rem;color:#5c4c30}
.glyphs{display:flex;gap:8px;direction:ltr}
.glyphs i{width:46px;height:56px;display:grid;place-items:center;font-style:normal;
  font-size:26px;line-height:1;border:1.5px solid rgba(18,62,119,.45);border-radius:10px;
  background:rgba(255,252,240,.6);color:#123e77;
  font-family:'Segoe UI Historic','Noto Sans Egyptian Hieroglyphs',serif}
.glyphs i u{text-decoration:none;font-family:var(--ff-m);font-size:10px;color:#8a5a20;
  display:block;margin-block-start:1px}
.riddle__io{display:flex;gap:9px;align-items:center;flex-wrap:wrap}
.riddle__io input{width:min(210px,50vw);padding:12px 16px;border-radius:999px;border:0;
  background:#fffcf0;box-shadow:inset 0 0 0 1.5px rgba(18,62,119,.4);font-size:1rem;
  color:#2b2316;outline:none;text-align:center}
.riddle__io input:focus{box-shadow:inset 0 0 0 2px #123e77}
.riddle__io .btn{background:#123e77;color:#f2e8cf;box-shadow:none}
.riddle__ok{display:none;font-size:1.02rem;color:#0d5c36;font-weight:600}
.riddle__no{display:none;font-size:.92rem;color:#c8502a}
.cartouche.is-solved .riddle__io,.cartouche.is-solved .riddle__no{display:none}
.cartouche.is-solved .riddle__ok{display:block}
.cartouche.is-solved{border-color:#0d5c36;box-shadow:0 0 0 4px rgba(13,92,54,.12)}

/* (D-050, user-authorised) The riddle used to announce "solved" while the last
   three tiles still read «?» — the payoff the whole puzzle is built toward
   never landed. Tiles 4-6 now fill with P·E·T.
   CSS-only by design: the markup and site.js inside #benha are protected, so
   the letters arrive as generated content rather than as a DOM edit. That also
   keeps the answer OUT of the unsolved page — the rules below only exist under
   `.is-solved`, so there is nothing to read in devtools before you solve it.
   An ANIMATION, not a transition, for the same reason: a transition would need
   a visible start state to interpolate from, which means shipping the letters
   before they are earned.
   The letters inherit face and colour from the `.glyphs i u` rule above, so
   they cannot drift from the already-solved A·N·U; `font-size` is the one value
   restated, because zeroing it on the <u> is what collapses the «?» away. */
@keyframes glyphFill{from{opacity:0;transform:rotateX(-90deg) translateY(-3px)}to{opacity:1;transform:none}}
.cartouche.is-solved .glyphs i:nth-child(n+4) u{font-size:0}
.cartouche.is-solved .glyphs i:nth-child(n+4) u::after{
  font-size:10px;display:inline-block;animation:glyphFill .45s var(--ease) both}
.cartouche.is-solved .glyphs i:nth-child(4) u::after{content:'P';animation-delay:.12s}
.cartouche.is-solved .glyphs i:nth-child(5) u::after{content:'E';animation-delay:.24s}
.cartouche.is-solved .glyphs i:nth-child(6) u::after{content:'T';animation-delay:.36s}
/* The page-wide reduced-motion block collapses animation-duration but not
   animation-delay, which would still deal the three letters out over 360ms.
   Reduced motion means the word is simply there. */
@media (prefers-reduced-motion:reduce){
  .cartouche.is-solved .glyphs i:nth-child(n+4) u::after{animation-delay:0s!important}
}
.cartouche.is-wrong{animation:shake .4s}
.cartouche.is-wrong .riddle__no{display:block}
@keyframes shake{25%{translate:-6px 0}50%{translate:6px 0}75%{translate:-4px 0}}

@media (max-width:1100px){
  .papyrus__in{grid-template-columns:1fr;padding-inline:clamp(26px,6vw,60px)}
  .papyrus__rail{display:none}
  .ankh{width:180px}
}
@media (max-width:560px){
  .glyphs i{width:38px;height:48px;font-size:22px}
  .stamp{width:112px;height:112px}
}

/* ══════════════════════════════════════════ motion overhaul (2026) ═══════
   Act I hero — character/word blur-fade reveal (#108) and a scroll-scrubbed
   supporting line (#82); Act II — a page-wide clip-path entrance language
   (#65), a velocity-driven slogan marquee (#103), a CSS-sticky track reveal (#126)
   and a static-position CTA language (luminous hover ring + one-shot click
   ripple, #110 — see D-036, buttons never move); Act III — the zoom approach
   into Benha (#142) and its hieroglyph-rail parallax (#66); Finale — the
   footer curtain (#85). Every rule here is additive: it layers on top of the
   existing selectors above rather than replacing them, so nothing here can
   regress a state some other rule already depends on. ═════════════════════ */

/* reveal units shared by the hero headline (#108) and the scroll-scrubbed
   supporting line (#82) — a plain inline box, so it carries none of the
   default browser styling <i>/<b> would add */
/* (r1 review) no blanket will-change here: the headline can carry well over
   a hundred units and promoting every one of them for the whole session
   pins that many composited layers long after their one entrance tween is
   done. The headline units get their promotion from the GSAP tween itself;
   only the supporting line — which is written on every scroll tick — asks
   for a standing one, and only for the two properties it actually writes. */
.rc{display:inline-block}

/* once JS hands the supporting line over to scroll-scrubbed reveal units,
   the CSS-only .rv fallback (opacity/translate on the whole paragraph) has
   to step aside — the parent stays put and only its .rc children animate */
.hero__s.is-scrubbed{opacity:1;transform:none}
.hero__s.is-scrubbed .rc{will-change:opacity,transform}
/* (r2 review) …and the promotion is released the moment the dissolve has
   run to completion — updateHeroSub() flips `is-spent` at remapped progress
   1 — so the units stop costing a compositor layer each for an effect that
   is over within the first screen of scroll. */
.hero__s.is-scrubbed.is-spent .rc{will-change:auto}

/* (C1) the about lead ignites word by word as the section is read into
   view: units rest in the muted body colour and resolve to full ink as the
   scrub passes over them. Colour only — no transform, no opacity — so the
   paragraph is fully readable at every point of the scrub, and the .rv
   fallback on the parent is released the moment JS takes the split over. */
.lead.is-scrubbed{opacity:1;transform:none}
/* display:inline, not the .rc default inline-block: the shared splitter cuts
   Latin runs per character, and adjacent inline-BLOCK boxes give the line
   breaker a break opportunity between every letter — a long English
   paragraph would start wrapping mid-word. This effect only animates colour,
   so plain inline boxes carry it perfectly and leave wrapping alone. */
.lead.is-scrubbed .rc{display:inline;color:var(--mut);transition:color .35s var(--ease2)}
.lead.is-scrubbed .rc.is-lit{color:var(--ink)}

/* progressive-enhancement gate (#65 + #85): only content behind this flag
   ever starts hidden, so a visit with JS disabled — or without
   IntersectionObserver — never ships a page the visitor cannot read; see
   the sectionReveal() gate in site.js */
html.js-motion .sect{
  clip-path:polygon(4% 6%,100% 0%,96% 100%,0% 94%);
  opacity:0;transform:scale(1.015);
  transition:clip-path 1s cubic-bezier(.35,.35,0,1),opacity .8s ease,transform 1s cubic-bezier(.35,.35,0,1);
  will-change:clip-path,opacity,transform;
}
html.js-motion .sect.is-in{clip-path:polygon(0 0,100% 0,100% 100%,0 100%);opacity:1;transform:none}
/* (r1 review) a permanently-parked clip-path breaks print and keeps a
   composited layer pinned on every section forever — sectionReveal() adds
   .is-done once the entrance transition actually finishes, and from then on
   the section is a plain box again */
html.js-motion .sect.is-done{clip-path:none;will-change:auto}
@media (max-width:640px){
  html.js-motion .sect{clip-path:polygon(2% 4%,100% 0%,98% 100%,0% 96%)}
}

/* kinetic slogan strip (#103) — skew is driven through GSAP's own transform
   cache (gsap.set in kineticMarquee()) so it composes with the marquee's x
   tween instead of the two fighting over the element's transform string */
.strip{will-change:transform}
#strip1{will-change:transform}

/* button feedback (D-036 rework) — buttons never move on hover any more, so
   the CTA language is now a luminous hover ring (see .btn--pri:hover /
   .btn--acc:hover above) plus a one-shot click ripple (#110 adapted, vanilla,
   bound in site.js). isolation:isolate keeps the ripple's own stacking
   context off the button's ancestors; .btn>* raises every child (label,
   icon) above the ripple, which is appended as the button's own last child. */
.btn--pri,.btn--acc{isolation:isolate}
.btn>*{position:relative;z-index:1}
/* `left`/`top` stay physical, never logical: site.js positions the ripple
   from the pointer's offset inside the button, which is a physical
   left-origin measurement in RTL just as much as in LTR. Width/height are
   set inline per button so scale(4) always overruns the widest CTA. */
.btn__ripple{position:absolute;z-index:0;top:0;left:0;width:10px;height:10px;
  border-radius:50%;pointer-events:none;background:rgba(8,20,38,.16);
  transform:translate(-50%,-50%) scale(0);opacity:.45;
  animation:btnRipple .7s ease-out forwards}
html[data-theme="dark"] .btn__ripple{background:rgba(255,255,255,.2)}
/* the filled CTAs carry a fixed navy/orange ground in both themes, so their
   ripple is the brighter white in both. (r2 review) They need a dark
   companion of their own precisely BECAUSE of that: the generic dark rule
   above scores (0,2,1) and outranks the unqualified `.btn--pri` selector at
   (0,2,0), so without this pair the primary CTA's ripple came out at .2 in
   dark mode — under half the intended brightness, on the darkest ground. */
.btn--pri .btn__ripple,.btn--acc .btn__ripple{background:rgba(255,255,255,.5)}
html[data-theme="dark"] .btn--pri .btn__ripple,
html[data-theme="dark"] .btn--acc .btn__ripple{background:rgba(255,255,255,.5)}
@keyframes btnRipple{to{transform:translate(-50%,-50%) scale(4);opacity:0}}

/* ══════════════════════════════════ standalone pages (terms/privacy/contact)
   A slim shell, not a second design language: every rule below is built from
   the same tokens and reuses the page's own primitives (.wrap, .eyebrow,
   .lead, .btn, .ci, .rv). It exists only to give those pages a header, a
   readable measure and a footer without pulling in the home page's loader,
   hero and scroll machinery. */
.page__top{position:sticky;inset-block-start:0;z-index:130;background:var(--bg2);
  border-block-end:1px solid var(--line)}
.page__top-in{display:flex;align-items:center;gap:10px;max-width:var(--maxw);
  margin-inline:auto;padding:12px var(--pad)}
.page__top-sp{margin-inline-start:auto}
.page__back{font-size:13.5px;font-weight:500;color:var(--mut);padding:8px 13px;border-radius:999px;
  transition:color .25s,background .25s}
.page__back:hover{color:var(--ink);background:var(--bg3)}

.page__head{padding-block:clamp(40px,5vw,78px) clamp(24px,2.6vw,38px);
  background:var(--bg2);border-block-end:1px solid var(--line)}
.page__head h1{margin-block:18px 16px}
/* These pages carry no `.rv` reveal classes on purpose: `.rv` starts at
   opacity 0 and is released by an IntersectionObserver, which would leave a
   visitor with JS off staring at a blank legal page. The only motion here is
   this load-time entrance — pure CSS, so it needs nothing to release it, and
   the reduced-motion block collapses it to nothing. */
/* transform only, never opacity: an animation that has not started yet
   holds its from-state, and in a background tab that would mean invisible
   legal text. A 14px offset is the worst case here, and it still reads. */
@keyframes pageIn{from{transform:translateY(14px)}to{transform:none}}
.page__head > .wrap > *{animation:pageIn .6s var(--ease) both}
.page__head > .wrap > *:nth-child(2){animation-delay:.07s}
.page__head > .wrap > *:nth-child(3){animation-delay:.14s}
.page__head > .wrap > *:nth-child(4){animation-delay:.21s}
.page__head .lead{max-width:62ch}
/* the draft flag has to read as a warning without the page looking broken */
.page__draft{display:inline-block;margin-block-end:20px;padding:7px 15px;border-radius:999px;
  font-family:var(--ff-m);font-size:12px;font-weight:600;letter-spacing:.04em;
  color:var(--orange-dk);background:rgba(255,107,53,.12);
  box-shadow:inset 0 0 0 1px rgba(255,107,53,.32)}
html[data-theme="dark"] .page__draft{color:#ffb191;background:rgba(255,107,53,.16)}

.page__body{padding-block:clamp(34px,4vw,64px) clamp(52px,6vw,96px);display:grid;gap:clamp(22px,2.4vw,34px)}
.page__sec{max-width:78ch}
.page__sec h2{margin-block-end:12px}
.page__sec p{color:var(--mut);font-size:1.03rem;line-height:1.9;margin-block-end:14px}
.page__sec .btn{margin-block-start:6px}
/* (owner · desktop) The "هتخرج بإيه" row on a course page is a grid written
   inline in site/course.blade.php, and it ships `align-items:start`. The
   comment above it defends `auto-fit`, and that argument is sound and left
   alone: how many of the five boxes render depends on which optional columns
   an admin filled, and a fixed column count would leave a hole. But that is an
   argument about how many COLUMNS there are. It says nothing about how tall
   the cards inside them should be, and `start` is the wrong answer to that.
   `.box` is a painted surface — `--bg2` under a 1px inset hairline — so
   unequal heights here are not a ragged text edge, they are four panels of
   four different sizes with a visible step between each: measured at 1280,
   the second row came out 429.4 / 274.1 / 467.3 / 327.5, a 193px spread.
   Stretched, the row reads as one band, and the empty tail inside the short
   card reads as what it is — a card with a shorter list in it.
   The selector matches the inline declaration because the row has no class of
   its own to hang on, and `!important` is the only weight that outranks a
   style attribute. If the view ever drops `align-items:start` this rule is the
   default restated and can go with it. */
.page__sec > div[style*="auto-fit"]{align-items:stretch!important}

/* An admin-authored page body (site_page_html) arrives class-less: its
   paragraphs are bare text nodes joined by <br>, its headings bare <h2>.
   These give a stored page exactly the typography the hardcoded fallback
   sections get, so a visitor cannot tell which of the two they are reading.
   The h2 size itself is shared with .h-md above rather than restated. */
.page__cms{color:var(--mut);font-size:1.03rem;line-height:1.9}
.page__cms h2{color:var(--ink);margin-block:30px 12px}
.page__cms > h2:first-child{margin-block-start:0}
/* site_page_html() joins EVERY line with <br>, the line after a heading
   included — invisible in the markup, an extra blank line on screen. */
.page__cms h2 + br{display:none}
.page__cta{display:flex;align-items:center;justify-content:space-between;gap:22px;flex-wrap:wrap;
  padding:clamp(22px,2.4vw,34px);border-radius:24px;background:var(--bg2);
  box-shadow:inset 0 0 0 1px var(--line)}
.page__cta p{color:var(--mut);margin-block-start:7px}
.page__soc{display:flex;gap:11px}
.page__soc a{width:44px;height:44px;border-radius:14px;display:grid;place-items:center;color:var(--mut);
  box-shadow:inset 0 0 0 1px var(--line);transition:color .3s,box-shadow .3s}
.page__soc a:hover{color:var(--navy);box-shadow:inset 0 0 0 1px var(--navy)}
html[data-theme="dark"] .page__soc a:hover{color:var(--sky);box-shadow:inset 0 0 0 1px var(--sky)}
.page__soc .ico{width:18px;height:18px}

/* a plain footer, deliberately NOT `.foot`: the home page's curtain reveal
   is bound to that class and these pages have no sentinel to drive it */
.page__foot{background:var(--bg2);border-block-start:1px solid var(--line)}
.page__foot-in{display:grid;grid-template-columns:1.6fr 1fr 1fr auto;gap:32px;
  max-width:var(--maxw);margin-inline:auto;padding:clamp(34px,3.4vw,54px) var(--pad)}
.page__foot-in p{font-size:.95rem;color:var(--mut);margin-block-start:9px;max-width:38ch}
.page__foot-links{display:grid;gap:10px}
.page__foot-links li{list-style:none}
.page__foot-links a{font-size:.95rem;color:var(--mut);transition:color .25s}
.page__foot-links a:hover{color:var(--ink)}
.page__foot-soc{display:flex;gap:10px;align-items:flex-start}
.page__foot-soc a{width:38px;height:38px;border-radius:12px;display:grid;place-items:center;
  color:var(--mut);box-shadow:inset 0 0 0 1px var(--line);transition:color .3s,box-shadow .3s}
.page__foot-soc a:hover{color:var(--navy);box-shadow:inset 0 0 0 1px var(--navy)}
html[data-theme="dark"] .page__foot-soc a:hover{color:var(--sky);box-shadow:inset 0 0 0 1px var(--sky)}
.page__foot-soc .ico{width:17px;height:17px}
.page__foot-bot{border-block-start:1px solid var(--line);font-size:13px;color:var(--mut)}
.page__foot-bot > span{display:block;max-width:var(--maxw);margin-inline:auto;padding:17px var(--pad)}
@media (max-width:900px){
  .page__foot-in{grid-template-columns:1fr 1fr}
  .page__cta{flex-direction:column;align-items:flex-start}
}
@media (max-width:560px){
  .page__foot-in{grid-template-columns:1fr}
}

/* footer curtain finale (#85) — #contact's rounded, shadowed edge tucks
   over the footer's top border for a static "curtain edge" read, and the
   footer itself rises into view through a clip-path wipe as it enters —
   the same mechanic the mobile drawer already uses, not a new one */
#contact{position:relative;z-index:2;margin-block-end:-28px;
  border-end-start-radius:28px;border-end-end-radius:28px;
  box-shadow:0 26px 56px rgba(8,20,38,.16)}
html[data-theme="dark"] #contact{box-shadow:0 26px 56px rgba(0,0,0,.5)}
.foot{position:relative;z-index:1}
html.js-motion .foot{clip-path:inset(0 0 100% 0);transition:clip-path 1.05s cubic-bezier(.16,1,.3,1)}
html.js-motion .foot.is-in{clip-path:inset(0 0 0% 0)}
@media (max-width:900px){
  #contact{margin-block-end:-16px;border-end-start-radius:20px;border-end-end-radius:20px}
}

/* ── /contact's message form (D-037) ────────────────────────────────────
   The form reuses the site's existing .form/.field/.row2/.ok/.form__err
   kit wholesale; only these two hooks are new. */

/* The form block sits above the .cinfo cards and must line up with them:
   .page__sec's 78ch reading measure is right for prose and wrong here, and
   a narrower form would sit visibly inset from the cards beneath it. */
.page__form{max-width:none}
.page__form .form{margin-block-start:22px}

/* Honeypot wrapper. Positioned off-screen rather than display:none or
   type="hidden" — see the field's own comment in site/contact.blade.php.
   !important is not used: nothing else in this sheet targets .hp. */
.hp{position:absolute;inset-inline-start:-9999px;inset-block-start:auto;
  width:1px;height:1px;overflow:hidden}

/* Course detail pane: the contact-vs-project hours breakdown. Sits under the
   facts row and carries the claim the hours position rests on. */
.pane__note{margin-block-start:14px;padding:12px 14px;border-radius:4px;
  background:rgba(var(--ink-rgb),.04);border-inline-start:3px solid var(--accent,var(--orange));
  font-size:14px;line-height:1.7;color:var(--mut)}

/* ═══════════════════════════════════════════════════ the ankh cursor ════
   The owner's ankh artwork as the pointer (owner, 2026-09-09), taken from the
   anubicompanion lab. A native `cursor` and no tracking loop: an element that
   chases the pointer in JS is always a frame behind it, and on a page that
   already runs two canvas players that frame is not free.

   Gated on `(hover: hover) and (pointer: fine)`, so a touch screen — which has
   no pointer to decorate — is never handed a cursor rule at all.

   17 8 is the hotspot, and it belongs to the artwork's geometry: the ankh is
   drawn into a 64x64 box tilted 32 degrees to the left, exactly like a mouse
   arrow leans, and 17,8 is where its tip lands. Changing the SVG's viewBox or
   its rotation without moving these numbers points the cursor at empty space.

   NOT ported from the lab with it: the falling code-rain, the click scarabs and
   the Eye of Horus seal that shared its stylesheet. The owner asked for the
   cursor.
   ════════════════════════════════════════════════════════════════════════ */
@media (hover: hover) and (pointer: fine) {
  html, body, body * {
    cursor: url("../ui/ankh-cursor.svg?v=ankh1") 17 8, auto !important;
  }
  a, button, [role="button"], summary, label[for] {
    cursor: url("../ui/ankh-cursor.svg?v=ankh1") 17 8, pointer !important;
  }
  /* The native caret survives wherever the visitor is placing it by eye. An I-beam
     is not decoration — it shows which character a click will land between, and an
     ankh over a text field takes that away. */
  textarea,
  input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]),
  [contenteditable="true"] {
    cursor: text !important;
  }
  /* The companion's drag grip keeps `grab`/`grabbing`. Nothing here reaches it:
     it lives in a shadow root and `body *` does not cross that boundary. That is
     the right outcome rather than an oversight — the grip's cursor is what says
     the mascot can be picked up. */
}
