:root {
  color-scheme: light;

  /* ==========================================================
     COLOUR
     See COLORS.md for the palette, the rules, and the contrast
     figures behind every pairing below.

     THE ONE RULE THAT MATTERS: bright fill, dark text.
     The brand and status colours are deliberately vivid, which
     means most of them do NOT reach 4.5:1 on white and must not
     be used for body-sized text. Every one therefore ships as a
     pair - a -fill for backgrounds, badges and buttons, and a
     -text sibling darkened just far enough to pass AA. Reach for
     the -text variant whenever the colour IS the letterform.
     ========================================================== */

  /* --- neutrals --- */
  --color-white: #ffffff;
  --color-surface: #f5f7fa;
  --color-surface-muted: #eef2f7;
  --color-surface-elevated: rgba(255, 255, 255, 0.92);
  --color-border: #e5e7eb;
  --color-border-strong: rgba(15, 23, 42, 0.16);

  --color-ink: #0f172a;
  --color-ink-muted: #4e5b6b;
  --color-ink-soft: #64748b;
  /* Faint is for non-text only - placeholders, hairlines, disabled glyphs.
     It does not reach 4.5:1 on any of our surfaces and must never carry
     body copy. */
  --color-ink-faint: #94a3b8;
  --color-border-soft: #cbd5e1;
  --color-chrome: #1c1e22;

  /* --- brand ---
     -hover is a straight darkening of the base; -text is the
     variant to use when the colour is applied to type. */
  --color-primary: #4d4dff;
  --color-primary-hover: #3a3abf;
  --color-primary-text: #4141d9;
  --color-primary-soft: #eaeaff;
  /* THE RED THEME. Every saturated red in the system resolves to one of
     these three values, and nothing else may introduce a fourth - the
     survey that produced them found 33 distinct red hexes across 95
     declarations, which is why "just pick a red" had drifted this far.

     base   #cb1f1f  the majority red: fills, buttons, bars, icons
     text   #cb2b2b  highlights and small parts (ink at body size)
     hover  #b31b1b  a step darker than base, for hover states

     -solid used to be a SEPARATE, darker tier (#de2f2f) because the old
     display red #ef3333 only carried white at 4.05:1. #cb1f1f carries it
     at 5.59:1 unaided, so that tier has nothing left to do and now
     aliases the base rather than being deleted - ~30 declarations still
     name it, and collapsing the value keeps them correct without a
     rename sweep through files this change does not otherwise touch. */
  --color-secondary: #cb1f1f;
  --color-secondary-hover: #b31b1b;
  --color-secondary-solid: #cb1f1f;
  --color-secondary-text: #cb2b2b;
  --color-secondary-soft: #fde9e9;

  /* The fixed header bar on all three shells - public .site-nav, student
     .student-nav, admin .admin-nav. Deliberately its own token rather
     than --color-secondary: that one is spread across ~200 declarations
     (buttons, badges, borders), so retuning the header through it would
     restyle the whole app. Kept translucent because every one of those
     three bars sets backdrop-filter: blur(14px), which does nothing
     behind an opaque background.

     DO NOT "fix" these channels to 203,31,31 to match #cb1f1f. They are
     already the theme base: at 0.96 over white this composites to exactly
     #cb1f1f (201*0.96 + 255*0.04 = 203.16 -> cb; 22*0.96 + 10.2 = 31.32
     -> 1f). Substituting the base's own channels here would render
     #cd2828 instead, i.e. writing the theme colour in would take the bar
     OFF theme. The header was the one red already sitting on the value
     the rest of the system has now been moved to. */
  --color-nav: rgba(201, 22, 22, 0.96);

  --focus-ring-color: #4d4dff;
  /* The unread-count badge is 10px white type on a small disc, which is
     the hardest place in the system to read a colour. It used to need
     the separate solid tier for that: the old display red #ef3333 gave
     white only 4.05:1, so #de2f2f (4.61) was carved out for it. The
     theme base clears both - white on #cb1f1f is 5.59:1 - so the badge
     now just takes the base red like everything else. */
  --badge-bg: #cb1f1f;
  --badge-text: #ffffff;

  /* --- status ---
     -fill  : badges, bars, icons, solid backgrounds
     -soft  : tinted panel backgrounds
     -text  : the same meaning, safe at body size on white
     Warning and error are deliberately DIFFERENT colours: an
     Incomplete application (fix it and resubmit) must not look
     like a Rejected one (final). */
  --status-success: #e4f8ec;
  --status-success-fill: #22c55e;
  --status-success-border: rgba(34, 197, 94, 0.32);
  --status-success-solid: #178841;
  --status-success-text: #16803d;

  --status-warning: #fef3e2;
  --status-warning-fill: #f59e0b;
  /* The amber fill carries WHITE text on .btn-warning by explicit design
     decision, at 2.15:1. That is below the 4.5:1 WCAG AA needs for text of
     this size, and is a deliberate, informed trade for the brighter button:
     white only reaches AA on amber once the fill is darkened to #a46a07,
     which reads olive rather than amber. Do not copy this pairing elsewhere -
     use --status-warning-solid (#a46a07) wherever white-on-amber must be
     legible. */
  --status-warning-on-fill: #ffffff;
  --status-warning-fill-hover: #d88b0a;
  --status-warning-border: rgba(245, 158, 11, 0.34);
  --status-warning-solid: #a46a07;
  --status-warning-text: #9a6407;

  /* Danger is the SAME red as the brand secondary above, and always was
     in practice - #ef3333 and #ef4444 differed by one channel step and
     nobody could tell them apart on screen. Both families now resolve to
     the three theme values, so the duplication is at least honest.
     -hover is new. Eighteen rules across admin/ and user/ hard-coded the
     literal 0xb03232 as the hover for a --status-danger-solid button,
     each one a copy of the last; the value they were all reaching for
     gets a name here so the nineteenth rule does not invent a
     nineteenth red. (Written 0x rather than as a hex literal on purpose:
     a bare one here gets caught by the next sweep that rewrites reds by
     value, and the comment silently becomes a lie about itself.)
     The tint is a background wash, not one of the three reds, so it keeps
     its own value - but there is now only ONE of it. Five near-duplicates
     (0xfbd5d5, 0xf3c9c9, 0xfecdca, 0xfedbd9, 0xfde7e7) were merged into
     it. Two of those were only ever borders, and went to
     --status-danger-border rather than here: flattening a border into the
     tint would have made it invisible against the panel it outlines.
     That merge is what lets -text be the single ink again. It reads
     4.59:1 on this tint; on the deepest of the old ones it was 3.97:1,
     which is why some rules had to borrow -hover as ink instead. Keep
     this value if you retune: lightening it costs contrast the -text tier
     has very little of to give. */
  --status-danger: #fde9e9;
  --status-danger-fill: #cb1f1f;
  --status-danger-border: rgba(203, 31, 31, 0.32);
  --status-danger-solid: #cb1f1f;
  --status-danger-hover: #b31b1b;
  --status-danger-text: #cb2b2b;

  --status-info: #e7f0fe;
  --status-info-fill: #3b82f6;
  --status-info-border: rgba(59, 130, 246, 0.32);
  --status-info-solid: #3472d8;
  --status-info-text: #3069c7;

  /* --- categorical, for analytics charts only ---
     Chart series need to be told apart from each other, which is
     a different job from signalling success or failure. Keeping
     them separate stops a chart series from being read as a
     status, and stops a status colour changing when someone
     restyles a graph. */
  --chart-1: #3b82f6;
  --chart-2: #8b5cf6;
  --chart-2-text: #7a51d8;
  --chart-3: #22c55e;
  --chart-4: #f59e0b;
  --chart-5: #cb1f1f;
  --chart-6: #559cfa;
  --chart-7: #db2777;

  --font-sans:
    "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    Helvetica, Arial, sans-serif;
  --text-xs: clamp(0.75rem, 0.72rem + 0.12vw, 0.8125rem);
  --text-sm: clamp(0.875rem, 0.84rem + 0.16vw, 0.95rem);
  --text-base: clamp(1rem, 0.96rem + 0.18vw, 1.0625rem);
  --text-lg: clamp(1.125rem, 1.04rem + 0.32vw, 1.25rem);
  --text-xl: clamp(1.35rem, 1.12rem + 0.85vw, 1.75rem);
  --text-2xl: clamp(1.65rem, 1.5rem + 0.7vw, 1.9rem);
  --text-3xl: clamp(1.9rem, 1.7rem + 1vw, 2.3rem);
  --text-display: clamp(2.35rem, 1.7rem + 3vw, 4.2rem);

  --space-1: clamp(0.25rem, 0.2rem + 0.2vw, 0.375rem);
  --space-2: clamp(0.5rem, 0.42rem + 0.32vw, 0.75rem);
  --space-3: clamp(0.75rem, 0.62rem + 0.45vw, 1rem);
  --space-4: clamp(1rem, 0.82rem + 0.65vw, 1.5rem);
  --space-5: clamp(1.5rem, 1.2rem + 1vw, 2.25rem);
  --space-6: clamp(2rem, 1.48rem + 1.75vw, 3.25rem);

  --radius-sm: 8px;
  --radius-md: 10px;
  --radius-lg: 12px;
  --radius-pill: 999px;

  --shadow-sm:
    0 1px 2px rgba(15, 23, 42, 0.06), 0 8px 22px rgba(15, 23, 42, 0.05);
  --shadow-md: 0 16px 38px rgba(15, 23, 42, 0.1);
  --shadow-lg: 0 24px 54px rgba(15, 23, 42, 0.14);

  --transition-fast: 160ms ease;
  --transition-base: 220ms ease;
  --transition-slow: 420ms cubic-bezier(0.2, 0.9, 0.35, 1);

  /* Legacy aliases retained for existing page-level styles. */
  --white: var(--color-white);
  --white-off: var(--color-surface);
  --red-primary: var(--color-secondary);
  --red-dark: var(--color-secondary-hover);
  --red-soft: var(--color-secondary-soft);
  --blue-primary: var(--color-primary);
  --blue-dark: var(--color-primary-hover);
  --blue-soft: var(--color-primary-soft);
  --gray-900: var(--color-ink);
  --gray-700: var(--color-ink-muted);
}

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

/* Password fields throughout the system use an application-owned visibility
   button. Hide Edge/legacy Microsoft native reveal controls so they do not
   occupy the same right-side space as the custom eye button. */
input[type="password"]::-ms-reveal,
input[type="password"]::-ms-clear {
  display: none;
}

* {
  margin: 0;
  padding: 0;
}

html {
  min-height: 100%;
  font-size: 100%;
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Keep fixed headers from shifting when page height changes the scrollbar. */
  scrollbar-gutter: stable;
}

body {
  min-height: 100vh;
  overflow-x: hidden;
  background:
    linear-gradient(
      135deg,
      rgba(248, 250, 252, 0.96),
      rgba(255, 255, 255, 0.9)
    ),
    var(--color-surface);
  color: var(--color-ink);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: 1.6;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

img,
picture,
svg,
video,
canvas {
  display: block;
  max-width: 100%;
}

button,
input,
textarea,
select {
  font: inherit;
}

button,
a {
  transition: background-color var(--transition-base),
    border-color var(--transition-base),
    box-shadow var(--transition-base),
    color var(--transition-base),
    opacity var(--transition-base),
    transform var(--transition-base);
}

a {
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--focus-ring-color);
  outline-offset: 2px;
}

::selection {
  background: rgba(0, 51, 204, 0.16);
  color: var(--color-ink);
}

.container {
  position: relative;
  z-index: 2;
  width: min(100% - (var(--space-4) * 2), 1400px);
  margin-inline: auto;
}

/* The public site's top navigation bar.

   Scoped to .site-nav, NOT a bare `nav` element selector. It used to be
   `nav { position: fixed; background: var(--color-secondary); ... }`, which hit EVERY <nav>
   in the app — including `<nav class="pagination">` in the admin results
   partials and the user-side announcement/notification pagination. Those
   controls were being torn out of the page flow and painted as a fixed red
   bar across the top of the viewport, which is exactly the "pagination looks
   broken" symptom. Keep this selector class-based. */
.site-nav {
  position: fixed;
  inset: 0 0 auto;
  z-index: 1000;
  min-height: 64px;
  padding: 0 var(--space-5);
  background: var(--color-nav);
  border-bottom: 1px solid rgba(255, 255, 255, 0.18);
  box-shadow: 0 10px 28px rgba(15, 23, 42, 0.12);
  backdrop-filter: blur(14px);
}

.nav-inner {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  max-width: 1400px;
  min-height: 64px;
  margin-inline: auto;
}

.nav-brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  justify-self: start;
}

.logo,
.logo-link {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Hidden on desktop - opens the mobile sidebar drawer with About/FAQs/
   Contact. Shown only at the 900px breakpoint below, where .nav-links
   is hidden. */
.public-nav-toggle {
  display: none;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  color: var(--color-white);
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background-color var(--transition-base);
}

.public-nav-toggle:hover {
  background: rgba(255, 255, 255, 0.18);
}

.public-nav-toggle.is-active {
  background: rgba(255, 255, 255, 0.22);
}

.public-nav-toggle i {
  font-size: 20px;
  transition: transform 0.25s ease;
}

.public-nav-toggle.is-active i {
  transform: rotate(90deg);
}

.logo-link {
  justify-self: start;
  margin-left: var(--space-5);
  color: var(--color-white);
  text-decoration: none;
  cursor: pointer;
}

.logo-round {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  overflow: hidden;
  background: var(--color-white);
  border: 1px solid rgba(255, 255, 255, 0.42);
  border-radius: 50%;
  box-shadow: 0 6px 16px rgba(15, 23, 42, 0.12);
  transition: transform var(--transition-base, 220ms ease),
    box-shadow var(--transition-base, 220ms ease);
}

.logo-link:hover .logo-round {
  transform: translateY(-1px) scale(1.05);
  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.18);
}

.logo-round img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.logo-text {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
  white-space: nowrap;
}

.logo-text-sub {
  color: rgba(255, 255, 255, 0.8);
  font-size: var(--text-xs);
  font-weight: 500;
}

.logo-text-main {
  color: var(--color-white);
  font-size: var(--text-base);
  font-weight: 800;
  letter-spacing: 0;
}

.logo-link:hover .logo-text {
  text-decoration: none;
}

.nav-center {
  justify-self: center;
  text-align: center;
}

.nav-center h2 {
  margin: 0;
  color: rgba(255, 255, 255, 0.94);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  margin-left: auto;
}

.nav-btns {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.btn-login,
.btn-signup,
.cta-primary,
.cta-contact {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px;
  border-radius: var(--radius-md);
  font-weight: 700;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
}

.nav-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0.65rem 0.15rem;
  color: rgba(255, 255, 255, 0.86);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.01em;
  text-decoration: none;
  white-space: nowrap;
}

.nav-link::after {
  content: "";
  position: absolute;
  left: 0.15rem;
  right: 0.15rem;
  bottom: 0.6rem;
  height: 2px;
  border-radius: var(--radius-pill);
  background: var(--color-white);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--transition-base);
}

.nav-link:hover,
.nav-link:focus-visible {
  color: var(--color-white);
}

.nav-link:hover::after,
.nav-link:focus-visible::after {
  transform: scaleX(1);
}

.nav-link.active {
  color: var(--color-white);
}

.nav-link.active::after {
  transform: scaleX(1);
}

.btn-login {
  padding: 0.6rem 1.1rem;
  color: var(--color-secondary-text);
  background: var(--color-white);
  border: 1.5px solid var(--color-white);
  font-size: var(--text-sm);
}

.btn-login:hover,
.btn-login:focus-visible {
  color: var(--color-white);
  background: var(--color-secondary-solid);
  border-color: var(--color-white);
  box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12);
  transform: translateY(-1px);
}

.btn-login.active {
  color: var(--color-secondary-text);
  background: var(--color-white);
  border-color: var(--color-white);
}

.btn-login.active:hover,
.btn-login.active:focus-visible {
  transform: translateY(-1px);
}

.btn-signup {
  padding: 0.6rem 1.15rem;
  margin-left: var(--space-1);
  color: var(--color-secondary-text);
  background: var(--color-white);
  border: 1.5px solid var(--color-white);
  box-shadow: 0 8px 18px rgba(15, 23, 42, 0.12);
  font-size: var(--text-sm);
}

.btn-signup:hover,
.btn-signup:focus-visible {
  color: var(--color-white);
  background: var(--color-secondary-solid);
  border-color: var(--color-white);
  transform: translateY(-1px);
}

.btn-signup.active {
  color: var(--color-white);
  background: var(--color-secondary-solid);
  border-color: var(--color-white);
}

.btn-signup.active:hover,
.btn-signup.active:focus-visible {
  transform: translateY(-1px);
}

.hero {
  display: flex;
  align-items: center;
  min-height: 100vh;
  padding: calc(64px + var(--space-6)) var(--space-4) var(--space-6);
}

.hero-inner {
  display: flex;
  align-items: center;
  width: min(100%, 1120px);
  margin-inline: auto;
  gap: var(--space-6);
}

.hero-left {
  flex: 1.2;
}

.contact-hero {
  border-radius: var(--radius-lg);
  transition: box-shadow var(--transition-slow),
    transform var(--transition-slow);
}

.contact-hero:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-md);
}

/* The default for any <h1>: a page title.
 *
 * This used to be the HERO rule - --text-display at weight 800 - applied to
 * every h1 in the system. Any page that dropped in a bare <h1> inherited a
 * 67px slab, and pages that overrode the size but not the weight (the
 * dashboard, the exam schedule, the FAQs) silently rendered at 800 while
 * their siblings sat at 700. That is where the "some titles look much
 * heavier" inconsistency came from.
 *
 * The hero now opts IN via .hero-heading rather than every page opting out.
 */
h1 {
  margin-bottom: var(--space-4);
  color: var(--color-ink);
  font-size: var(--text-xl);
  font-weight: 700;
  letter-spacing: 0;
  line-height: 1.2;
}

/* The one place --text-display belongs: the landing hero. Weight 800 is
 * reserved for this and for marketing section titles - see TYPOGRAPHY.md. */
.hero-heading {
  font-size: var(--text-display);
  font-weight: 800;
  line-height: 1.08;
}

.heading-line {
  display: block;
}

.heading-line-ink {
  color: var(--color-ink);
}

.heading-line-red {
  color: var(--color-secondary);
}

.heading-line-blue {
  color: var(--color-primary);
}

.hero-desc {
  max-width: 560px;
  margin-bottom: var(--space-5);
  color: var(--color-ink-muted);
  font-size: var(--text-lg);
  line-height: 1.65;
}

.hero-cta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.cta-primary,
.cta-contact {
  color: var(--color-white);
  border: 1px solid transparent;
  background: var(--color-secondary-solid);
  border-radius: var(--radius-pill);
  box-shadow: 0 10px 22px rgba(203, 31, 31, 0.22);
}

.cta-primary {
  padding: 0.9rem 1.6rem;
  font-size: var(--text-base);
}

.cta-primary:hover,
.cta-contact:hover {
  background: var(--color-secondary-hover);
  box-shadow: 0 14px 28px rgba(203, 31, 31, 0.26);
  transform: translateY(-2px);
}

.cta-primary i {
  transition: transform var(--transition-base);
}

.cta-primary:hover i {
  transform: translateX(4px);
}

.hero-logo-slot {
  position: relative;
  flex: 1.15;
  max-width: 620px;
}

.hero-logo-frame {
  position: relative;
}

.hero-logo-frame img {
  position: relative;
  z-index: 1;
  width: min(100%, 560px);
  height: auto;
  margin-inline: auto;
  filter: drop-shadow(0 16px 28px rgba(15, 23, 42, 0.12));
  transition: transform var(--transition-base), filter var(--transition-base);
}

.hero-logo-frame:hover img {
  transform: scale(1.04) translateY(-6px);
  filter: drop-shadow(0 24px 36px rgba(15, 23, 42, 0.2));
}

.contact-hero {
  flex: 1;
  width: min(100%, 700px);
  margin: var(--space-4) auto;
  padding: var(--space-4);
}

.contact-hero-inner {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  width: 100%;
}

.contact-hero-left {
  flex: 1;
}

.contact-title {
  margin-bottom: var(--space-2);
  color: var(--color-ink);
  font-size: var(--text-xl);
  font-weight: 700;
  letter-spacing: 0;
}

.contact-subtitle {
  max-width: 560px;
  margin-bottom: var(--space-4);
  color: var(--color-ink-muted);
  font-size: var(--text-base);
}

.contact-details {
  margin-bottom: var(--space-4);
}

.contact-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
  color: var(--color-ink-muted);
  font-size: var(--text-base);
}

.contact-icon {
  flex: 0 0 28px;
  width: 28px;
  color: var(--color-secondary);
  font-size: 1.2rem;
}

.cta-contact {
  padding: 0.8rem 1.2rem;
  font-size: var(--text-sm);
}

/* The .alert-* selectors that used to be grouped into these rules now live in
   assets/css/components/alerts.css. What remains are the standalone status
   utilities, which are applied to things that are not alerts at all — so they
   keep the flat tinted treatment rather than inheriting the alert card.
   The .alert.ok / .alert.error / .alert.warn aliases were dropped outright:
   no markup in the project has ever used them. */
.success {
  color: var(--status-success-text);
  background: var(--status-success);
  border-color: var(--status-success-border);
}

.danger,
.error {
  color: var(--status-danger-text);
  background: var(--status-danger);
  border-color: var(--status-danger-border);
}

.warning {
  color: var(--status-warning-text);
  background: var(--status-warning);
  border-color: var(--status-warning-border);
}

.info {
  color: var(--status-info-text);
  background: var(--status-info);
  border-color: var(--status-info-border);
}

footer {
  position: relative;
  z-index: 3;
  width: 100%;
  margin-top: var(--space-5);
  padding: var(--space-5) var(--space-4) var(--space-3);
  color: var(--color-white);
  background: #000081;
}

.footer-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-4);
  width: min(100%, 1280px);
  margin: 0 auto var(--space-3);
}

.footer-col {
  min-width: 0;
}

.footer-col h4 {
  margin-bottom: var(--space-2);
  color: var(--color-white);
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0;
}

.footer-logos {
  display: flex;
  gap: 0.6rem;
  margin-bottom: var(--space-3);
}

.footer-logos img {
  box-sizing: border-box;
  width: 52px;
  height: 52px;
  object-fit: contain;
  padding: 3px;
  border-radius: 50%;
  background-color: var(--color-white);
}

.footer-col p,
.footer-col ul {
  color: rgba(255, 255, 255, 0.78);
  font-size: var(--text-xs);
  line-height: 1.6;
}

.footer-col ul {
  list-style: none;
}

.footer-link-columns {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
}

.footer-col ul li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: var(--space-1);
}

.footer-col ul li svg {
  flex: 0 0 auto;
  width: 15px;
  height: 15px;
  stroke-width: 2;
  color: rgba(255, 255, 255, 0.78);
}

.footer-col ul li a {
  color: rgba(255, 255, 255, 0.86);
  text-decoration: none;
  transition: color var(--transition-base);
  display: inline-block;
}

.footer-col ul li > span,
.footer-col ul li > a {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* The phone number is the one footer link worth an oversized thumb target, and
   it is also the only one that can safely have it: its neighbours in the
   Contact column are plain <span>s, so an overhanging hit area has no adjacent
   target to steal a tap from. The negative margin cancels the padding's effect
   on row height, so the row still measures the same 19px as the spans above
   and below it.

   Every other footer link used to carry this same padding. In Quick Links -
   six links stacked 4px apart - it made each row 35px against Contact's 19px,
   and cancelling it there WOULD overlap neighbouring targets by 12px, so the
   padding is simply dropped rather than hidden. */
.footer-col ul li a[href^="tel:"] {
  padding: 8px 0;
  margin: -8px 0;
}

/* Keep footer links bright against the dark navy footer on hover/focus. */
.footer-col ul li a:hover,
.footer-col ul li a:focus-visible {
  color: #bfdbfe;
  background: none;
}

.footer-bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: min(100%, 1280px);
  margin-inline: auto;
  padding-top: var(--space-3);
  border-top: 1px solid rgba(255, 255, 255, 0.16);
  color: rgba(255, 255, 255, 0.68);
  font-size: var(--text-xs);
}

.footer-credit {
  white-space: nowrap;
}

/* The copyright is one long sentence in a narrow bar. Left to wrap on its
   own it broke after "All rights" below ~480px and stranded "reserved."
   alone on the second line. Keeping the rights clause unbreakable (same
   trick as .footer-credit above) moves the wrap to the sentence boundary
   instead, at every phone width. text-wrap:balance then evens the two
   lines where it is supported and is simply ignored where it isn't, so
   the nowrap span is what actually guarantees the fix. */
.footer-bottom p {
  text-wrap: balance;
}

.footer-rights {
  white-space: nowrap;
}

@media (max-width: 520px) {
  .footer-bottom {
    flex-direction: column;
    text-align: center;
  }
}

.fade-up {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.75s ease,
    transform 0.75s cubic-bezier(0.2, 0.9, 0.35, 1);
}

.fade-up.visible,
.fade-up.initial-visible {
  opacity: 1;
  transform: translateY(0);
}

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

@media (max-width: 900px) {
  .container {
    width: min(100% - (var(--space-3) * 2), 1400px);
  }

  .site-nav {
    padding-inline: var(--space-3);
  }

  .nav-center,
  .nav-links {
    display: none;
  }

  .public-nav-toggle {
    display: inline-flex;
  }

  .nav-btns {
    gap: var(--space-3);
    margin-left: auto;
  }

  .nav-btns .btn-login {
    display: none;
  }

  .btn-signup {
    min-height: 44px;
    padding: 0.58rem 0.85rem;
    font-size: var(--text-xs);
  }

  .logo-text {
    font-size: var(--text-xs);
  }

  .hero-inner,
  .contact-hero-inner {
    flex-direction: column;
    text-align: center;
    gap: var(--space-4);
  }

  .hero-logo-slot {
    max-width: min(70%, 360px);
    margin-inline: auto;
  }

  .hero-left,
  .contact-hero-left {
    text-align: center;
  }

  .hero-desc,
  .contact-subtitle {
    margin-inline: auto;
  }

  .hero-cta,
  .contact-cta {
    justify-content: center;
  }

  .contact-hero {
    padding: var(--space-4);
  }

  .footer-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    text-align: center;
  }

  .footer-brand {
    grid-column: 1 / -1;
  }

  .footer-logos {
    justify-content: center;
  }
}

/* Mobile menu - dropdown panel opened by .public-nav-toggle once
   .nav-links is hidden at the 900px breakpoint above. The red nav bar
   (logo + toggle) stays fixed on top and visible; the panel unfurls
   beneath it and its links cascade in top to bottom. */
.public-sidebar-backdrop {
  display: none;
}

body.public-sidebar-locked {
  overflow: hidden;
}

.public-sidebar {
  position: fixed;
  top: 64px;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 999;
  display: flex;
  flex-direction: column;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(250, 250, 252, 0.99));
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  box-shadow: 0 28px 50px rgba(15, 23, 42, 0.16);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(-16px);
  transition: opacity 0.3s ease,
    transform 0.35s cubic-bezier(0.16, 1, 0.3, 1),
    visibility 0s linear 0.35s;
}

.public-sidebar.is-open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateY(0);
  transition: opacity 0.3s ease,
    transform 0.35s cubic-bezier(0.16, 1, 0.3, 1),
    visibility 0s linear 0s;
}

.public-sidebar-nav {
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: var(--space-6) var(--space-5) 0;
  overflow-y: auto;
}

.public-sidebar-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: 60px;
  padding: var(--space-3) 0.25rem;
  color: var(--color-ink);
  border-bottom: 1px solid rgba(15, 23, 42, 0.1);
  text-decoration: none;
  opacity: 0;
  transform: translateY(-14px);
  transition: color var(--transition-base),
    border-color var(--transition-base),
    opacity 0.35s ease,
    transform 0.35s ease;
}

.public-sidebar-link-text {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.public-sidebar-link-title {
  font-size: var(--text-lg);
  font-weight: 500;
}

.public-sidebar-link-desc {
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  font-weight: 500;
  transition: color var(--transition-base);
}

.public-sidebar-link:hover .public-sidebar-link-desc,
.public-sidebar-link.active .public-sidebar-link-desc {
  color: rgba(203, 31, 31, 0.7);
}

.public-sidebar.is-open .public-sidebar-link {
  opacity: 1;
  transform: translateY(0);
}

.public-sidebar.is-open .public-sidebar-link:nth-child(1) {
  transition-delay: 0.08s;
}

.public-sidebar.is-open .public-sidebar-link:nth-child(2) {
  transition-delay: 0.14s;
}

.public-sidebar.is-open .public-sidebar-link:nth-child(3) {
  transition-delay: 0.2s;
}

.public-sidebar-link:hover,
.public-sidebar-link.active {
  color: var(--color-secondary);
  border-color: rgba(203, 31, 31, 0.3);
}

.public-sidebar-chevron {
  font-size: 13px;
  color: rgba(15, 23, 42, 0.25);
  transform: translateX(-4px);
  opacity: 0;
  transition: color var(--transition-base),
    transform var(--transition-base),
    opacity var(--transition-base);
}

.public-sidebar-link:hover .public-sidebar-chevron,
.public-sidebar-link.active .public-sidebar-chevron {
  color: var(--color-secondary);
  transform: translateX(0);
  opacity: 1;
}

.public-sidebar-footer {
  flex-shrink: 0;
  padding: var(--space-5) var(--space-5) var(--space-4);
  opacity: 0;
  transform: translateY(-14px);
  transition: opacity 0.35s ease, transform 0.35s ease;
}

.public-sidebar.is-open .public-sidebar-footer {
  opacity: 1;
  transform: translateY(0);
  transition-delay: 0.26s;
}

.public-sidebar-login-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 52px;
  color: var(--color-white);
  background: linear-gradient(135deg, var(--color-secondary), var(--color-secondary-hover));
  border: none;
  border-radius: var(--radius-pill);
  box-shadow: 0 12px 26px rgba(203, 31, 31, 0.3);
  font-size: var(--text-base);
  font-weight: 700;
  text-decoration: none;
  transition: transform var(--transition-base),
    box-shadow var(--transition-base);
}

.public-sidebar-login-btn:hover,
.public-sidebar-login-btn:focus-visible {
  transform: translateY(-2px);
  box-shadow: 0 16px 32px rgba(203, 31, 31, 0.38);
}

.public-sidebar-subtext {
  margin: var(--space-3) 0 0;
  color: var(--color-ink-muted);
  font-size: var(--text-sm);
  text-align: center;
}

.public-sidebar-subtext a {
  color: var(--color-secondary);
  font-weight: 700;
  text-decoration: none;
}

.public-sidebar-subtext a:hover,
.public-sidebar-subtext a:focus-visible {
  text-decoration: underline;
}

@media (max-width: 520px) {
  .footer-grid {
    grid-template-columns: 1fr;
  }

  .footer-brand {
    grid-column: auto;
  }

  .site-nav {
    min-height: 60px;
  }

  .public-sidebar {
    top: 60px;
  }

  .logo-round {
    width: 38px;
    height: 38px;
  }

  .nav-btns {
    flex-wrap: wrap;
    justify-content: flex-end;
  }

  .hero {
    padding-top: calc(60px + var(--space-5));
  }

  .btn-login {
    min-height: 34px;
    padding-inline: 0.5rem;
  }

  .btn-login i,
  .btn-signup i {
    display: none;
  }

  .btn-signup {
    min-height: 34px;
    padding-inline: 0.6rem;
  }
}

/* Keep the public one-row navigation aligned with the student and standalone
   admin shells on phones. The full admin dashboard remains taller because it
   adds a separate mobile page-title row. */
@media (max-width: 768px) {
  .site-nav,
  .nav-inner {
    min-height: 60px;
  }

  .public-sidebar {
    top: 60px;
  }

  .hero {
    padding-top: calc(60px + var(--space-5));
  }
}

/* ==========================================
   SWEETALERT2 - LOGOUT CONFIRMATION
   Shared theme for admin_footer.php and
   user_footer.php so both surfaces match the
   rest of the app instead of stock SweetAlert2.
========================================== */
.iskolar-swal-popup {
  width: 380px !important;
  padding: var(--space-5) var(--space-4) var(--space-4) !important;
  border-radius: var(--radius-lg) !important;
  box-shadow: var(--shadow-lg) !important;
  font-family: var(--font-sans) !important;
}

@media (max-width: 600px) {
  .iskolar-swal-popup {
    box-sizing: border-box !important;
    width: calc(100% - 2rem) !important;
    max-width: 380px !important;
  }

  .iskolar-swal-actions {
    flex-direction: row !important;
    align-items: center !important;
    flex-wrap: nowrap !important;
    gap: 0.5rem !important;
  }

  .iskolar-swal-confirm,
  .iskolar-swal-cancel {
    flex: 1 1 0 !important;
    width: auto !important;
    min-width: 0 !important;
    min-height: 44px !important;
    padding-inline: 0.5rem !important;
    white-space: nowrap !important;
  }

  /* Some user-side confirmations (for example notification deletion) use
     SweetAlert's stock classes instead of the shared themed class names.
     Keep those actions in the same compact, side-by-side layout and prevent
     labels from becoming two lines at the narrowest supported width. */
  .swal2-container .swal2-popup:not(.iskolar-swal-popup) {
    box-sizing: border-box !important;
    width: calc(100% - 2rem) !important;
    max-width: 380px !important;
  }

  .swal2-container .swal2-actions {
    flex-wrap: nowrap !important;
    gap: 0.5rem !important;
    width: 100% !important;
  }

  .swal2-container .swal2-actions .swal2-styled {
    flex: 1 1 0 !important;
    min-width: 0 !important;
    margin: 0 !important;
    padding-inline: 0.5rem !important;
    white-space: nowrap !important;
  }
}

/* The dialog arrives with the same short fade as the toast. SweetAlert's
   default swal2-show scales past 1 and settles back, which on a
   confirm-before-you-delete dialog reads as playfulness in the wrong place. */
.swal2-container .iskolar-swal-popup.swal2-show {
  animation: iskolarDialogIn 0.16s ease-out !important;
}

.swal2-container .iskolar-swal-popup.swal2-hide {
  animation: iskolarDialogOut 0.12s ease-in forwards !important;
}

@keyframes iskolarDialogIn {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

@keyframes iskolarDialogOut {
  to { opacity: 0; transform: translateY(4px); }
}

.iskolar-swal-icon {
  display: grid !important;
  place-items: center !important;
  box-sizing: border-box !important;
  width: 60px !important;
  height: 60px !important;
  margin-top: 0 !important;
  margin-bottom: var(--space-3) !important;
  border: none !important;
  background: var(--color-secondary-soft) !important;
  color: var(--color-secondary) !important;
  animation: none !important;
}

/* Same reasoning as the toast: SweetAlert's drawn marks are positioned for
   its own 80px icon and animate into place, so at 60px inside a tinted
   circle they sit off-centre and swing. The dialog passes iconHtml where it
   wants a Font Awesome glyph; where it doesn't, one static character. */
.iskolar-swal-icon *:not(.swal2-icon-content):not(i):not(svg) {
  display: none !important;
  animation: none !important;
}

/* line-height is the one that matters here. SweetAlert sets 80px on the icon
   content to centre it inside its own 80px icon; in this 60px circle that
   leaves an 80px-tall box whose glyph sits 10px below the true centre — close
   enough to look like a mistake rather than a bug. Collapsing the line box to
   the glyph lets the grid do the centring exactly. */
.iskolar-swal-icon .swal2-icon-content,
.iskolar-swal-icon i {
  grid-area: 1 / 1;
  align-self: center !important;
  justify-self: center !important;
  font-size: 26px !important;
  line-height: 1 !important;
  animation: none !important;
}

.iskolar-swal-icon .swal2-icon-content {
  font-size: 1.6rem !important;
}

.iskolar-swal-title {
  padding: 0 0 var(--space-2) !important;
  color: var(--color-ink) !important;
  font-size: var(--text-lg) !important;
  font-weight: 700 !important;
}

.iskolar-swal-text {
  padding: 0 !important;
  color: var(--color-ink-muted) !important;
  font-size: var(--text-sm) !important;
  line-height: 1.5 !important;
}

.iskolar-swal-actions {
  gap: var(--space-2) !important;
  margin-top: var(--space-4) !important;
}

.iskolar-swal-confirm,
.iskolar-swal-cancel {
  margin: 0 !important;
  padding: 10px 22px !important;
  border-radius: var(--radius-pill) !important;
  font-size: var(--text-sm) !important;
  font-weight: 600 !important;
  box-shadow: none !important;
}

.iskolar-swal-confirm {
  background: var(--color-secondary-solid) !important;
  color: var(--color-white) !important;
  order: 2;
}

.iskolar-swal-confirm:hover,
.iskolar-swal-confirm:focus-visible {
  background: var(--color-secondary-hover) !important;
}

.iskolar-swal-cancel {
  background: var(--color-surface-muted) !important;
  color: var(--color-ink-muted) !important;
  order: 1;
}

.iskolar-swal-cancel:hover,
.iskolar-swal-cancel:focus-visible {
  background: var(--color-border) !important;
  color: var(--color-ink) !important;
}

/* ========== FLASH TOAST ==========
   admin_footer.php has asked SweetAlert for this class since the flash
   channel was written, but nothing ever defined it — so every "saved
   successfully" toast in the admin panel has been rendering as stock
   SweetAlert2 rather than as this system. Defined here in the shared
   stylesheet so the student-side flash channel inherits the same look.

   Matches the alert component (assets/css/components/alerts.css): white
   card, quiet border, severity carried by the icon rather than by tinting
   the whole toast. */
.iskolar-swal-toast {
  width: auto !important;
  max-width: 340px !important;
  padding: var(--space-3) !important;
  border: 1px solid var(--color-border) !important;
  border-radius: var(--radius-md, 10px) !important;
  background: var(--color-white) !important;
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.16) !important;
  font-family: var(--font-sans) !important;
}

.iskolar-swal-toast .swal2-title {
  margin: 0 !important;
  padding: 0 !important;
  color: var(--color-ink) !important;
  font-size: var(--text-sm) !important;
  font-weight: 600 !important;
  line-height: 1.45 !important;
  text-align: left !important;
}

/* Toasts land 2px from the top-right corner by default, which puts them on
   top of the fixed header — covering the account menu, which is exactly
   where someone's eye goes after saving something from it.
   Every area's desktop bar is 64px. Mobile admin uses a two-row 104px shell,
   while the one-row public/student bars use 60px. This offset clears the
   tallest common desktop bar and drops the toast into the content
   area just below, out of the header's way in all three. */
:root {
  --toast-top-offset: 84px;
}

.swal2-container.swal2-top-end,
.swal2-container.swal2-top-right {
  padding: var(--toast-top-offset) var(--space-3) 0 0 !important;
}

/* Admin's mobile header/title bar is a fixed z-index: 1200 stacking
   context. SweetAlert2's container is normally below that layer, so a toast
   can be painted behind the title even when the toast popup itself has a
   large z-index. Scope the higher layer to admin pages; the container is a
   direct child of body and therefore can be lifted as a whole. */
@media (max-width: 768px) {
  .admin-page .swal2-container {
    z-index: 2100 !important;
  }
}

/* Entrance. SweetAlert's default swal2-show is a bounce — scale 0.7 → 1.05
   → 0.95 → 1. On a full-screen modal that reads as arrival; on a small toast
   in the corner of the screen it reads as a twitch. A short fade and slide
   says the same thing without the wobble. */
.swal2-container .iskolar-swal-toast.swal2-show {
  animation: iskolarToastIn 0.18s ease-out !important;
}

.swal2-container .iskolar-swal-toast.swal2-hide {
  animation: iskolarToastOut 0.15s ease-in forwards !important;
}

@keyframes iskolarToastIn {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: none; }
}

@keyframes iskolarToastOut {
  to { opacity: 0; transform: translateY(-4px); }
}

/* The icon.
   SweetAlert builds its success and error marks out of absolutely-positioned
   pieces — a ring, two line segments, a masking wedge — laid out for its
   default 80px icon and animated into place. Forced down to toast size those
   pieces keep their original offsets, so the checkmark sits low and left of
   its ring and then swings as it draws.
   One centred glyph instead: no moving parts to land in the wrong place, and
   the same four status colours the alert component uses. */
.iskolar-swal-toast .swal2-icon {
  display: grid !important;
  place-items: center !important;
  box-sizing: border-box !important;
  width: 22px !important;
  height: 22px !important;
  min-width: 22px !important;
  margin: 0 var(--space-2) 0 0 !important;
  border-width: 2px !important;
  animation: none !important;
}

.iskolar-swal-toast .swal2-icon * {
  display: none !important;
  animation: none !important;
}

.iskolar-swal-toast .swal2-icon::after {
  grid-area: 1 / 1;
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
}

.iskolar-swal-toast .swal2-icon.swal2-success {
  border-color: var(--status-success-text) !important;
  color: var(--status-success-text) !important;
}

.iskolar-swal-toast .swal2-icon.swal2-success::after {
  content: "\2713"; /* check */
}

.iskolar-swal-toast .swal2-icon.swal2-error {
  border-color: var(--status-danger-text) !important;
  color: var(--status-danger-text) !important;
}

.iskolar-swal-toast .swal2-icon.swal2-error::after {
  content: "\2715"; /* cross */
}

.iskolar-swal-toast .swal2-icon.swal2-warning {
  border-color: var(--status-warning-text) !important;
  color: var(--status-warning-text) !important;
}

.iskolar-swal-toast .swal2-icon.swal2-warning::after {
  content: "!";
}

.iskolar-swal-toast .swal2-icon.swal2-info {
  border-color: var(--status-info-text) !important;
  color: var(--status-info-text) !important;
}

.iskolar-swal-toast .swal2-icon.swal2-info::after {
  content: "i";
}

.iskolar-swal-toast .swal2-timer-progress-bar {
  background: var(--color-border-strong) !important;
}

@media (prefers-reduced-motion: reduce) {
  .swal2-container .iskolar-swal-toast.swal2-show,
  .swal2-container .iskolar-swal-toast.swal2-hide {
    animation: none !important;
  }
}

/* ========== ICON-ONLY ACTION BUTTON ==========
   Same colours as the full-width buttons — just no text label, with the
   title attribute carrying the name instead. Used wherever a table has more
   actions than it has room for labels (Exams, Access Control). Lives here
   rather than in a page stylesheet so the two pages cannot drift apart. */
.btn-icon-only {
  width: 30px;
  height: 30px;
  padding: 0;
  gap: 0;
  justify-content: center;
  flex: 0 0 auto;
}

/* ========== SHARED BUTTON LOADING STATE ========== */
.btn-spinner {
  display: inline-block;
  width: 14px;
  height: 14px;
  border: 2px solid rgba(255, 255, 255, 0.35);
  border-top-color: var(--color-white);
  border-radius: 50%;
  animation: btn-spin 0.6s linear infinite;
  vertical-align: middle;
}

@keyframes btn-spin {
  to { transform: rotate(360deg); }
}

button.is-loading {
  opacity: 0.85;
  cursor: not-allowed;
  pointer-events: none;
}

/* ==========================================================================
   PAGE LOADING OVERLAY
   Shown while a navigation or a slow form submit is in flight, so a click
   that takes a moment doesn't look like nothing happened.

   This used to draw a skeleton of the page it was about to load. That was a
   guess made from the clicked href, on the page being LEFT, before the
   destination existed — so it drew a generic five-column grid over pages that
   actually open with a title row, a tab bar, five stat cards and a filter
   panel. The placeholder never matched what arrived, and the mismatch landed
   as a jolt at exactly the moment the real page appeared.

   So it no longer guesses. Indeterminate progress says "something is coming"
   without claiming to know its shape, which is all this overlay is in a
   position to know. Skeletons still exist — in skeleton.js, for the AJAX
   flows that can MEASURE the DOM they are replacing.
   ========================================================================== */
.page-loader {
  position: fixed;
  /* Whole viewport by default. page-loader.js writes inline top/left to pull
     these edges past whatever fixed chrome the surface has — the admin and
     student headers and sidebars, and the logged-out .site-nav. That chrome is
     identical on the next page, so covering it only made it flicker away and
     back. The default only survives on a page with no fixed header at all. */
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 99999;
  overflow: hidden;
  /* Translucent, not opaque. You are still on the page you clicked from;
     washing it out to near-white pretended otherwise. Dimming reads as
     "inert, and something is on the way" without erasing anything. */
  background: rgba(255, 255, 255, 0.55);
  backdrop-filter: blur(1px);
  /* Hidden by default in BOTH senses: opacity for the fade, visibility so it
     can never intercept clicks while invisible. */
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.18s ease, visibility 0.18s ease;
}

.page-loader.is-visible {
  opacity: 1;
  visibility: visible;
}

/* When a recorded shape is available the overlay stops being a wash over the
   old page and becomes the new page arriving: opaque ground, real panels,
   marks where the text will be. The bar stays on top of it, because a
   skeleton says what is coming and the bar says it is still coming. */
.page-loader.has-shape {
  background: transparent;
  backdrop-filter: none;
}

/* The replayed layer. Its origin is the content column's top-left — the same
   point page-loader.js frames the overlay to — so recorded coordinates drop
   straight in with no translation. */
.skeleton-shape {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--admin-workspace-bg, var(--color-surface));
}

.skeleton-shape-surface {
  position: absolute;
  background: var(--color-white);
  border: 1px solid rgba(15, 23, 42, 0.07);
}

/* Carries the ox/oy offset for a shape whose content did not start at the
   frame's own origin — a centred .container on the logged-out pages, rather
   than the admin and student columns where the two coincide. A zero-sized
   positioned box: the marks inside resolve their left/top against its origin,
   while .skeleton-shape above keeps filling the whole frame so no strip of
   the previous page can show through beside it. */
.skeleton-shape-group {
  position: absolute;
  left: 0;
  top: 0;
}

/* Inherits .skeleton for the base fill and the moving sheen; this only
   places it. Width, height and radius are inline, straight from the
   measurement.

   Doubled selector on purpose: components/skeleton.css loads AFTER this file
   and sets .skeleton { position: relative }, so a single class here would
   lose the tie on source order and every mark would stack in a column. */
.skeleton.skeleton-shape-ink {
  position: absolute;
}

/* Rides the top edge of whatever the overlay covers — the top of the content
   column on a logged-in page, the top of the window otherwise. */
.page-loader-bar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  overflow: hidden;
  background: var(--color-primary-soft, rgba(0, 51, 204, 0.1));
}

.page-loader-bar-fill {
  position: absolute;
  inset: 0;
  transform-origin: left center;
  background: var(--color-primary, var(--color-primary));
  animation: page-loader-indeterminate 1.15s ease-in-out infinite;
}

/* Transform only — no left/width animation, which would lay out on every
   frame while the browser is already busy fetching a page. */
@keyframes page-loader-indeterminate {
  0%   { transform: translateX(-100%) scaleX(0.35); }
  50%  { transform: translateX(-15%) scaleX(0.6); }
  100% { transform: translateX(100%) scaleX(0.35); }
}

/* Only rendered when a caller passes a message to showPageLoader(). The bar
   alone carries the ordinary case. */
.page-loader-text {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  transform: translateY(-50%);
  padding: 0 1rem;
  text-align: center;
  font-size: 14px;
  font-weight: 600;
  color: var(--color-primary, var(--color-primary));
  letter-spacing: 0.01em;
}

.page-loader-text[hidden] {
  display: none;
}

/* Admin panel is red rather than blue. */
.page-loader.page-loader-admin .page-loader-bar {
  background: rgba(203, 31, 31, 0.12);
}

.page-loader.page-loader-admin .page-loader-bar-fill {
  background: var(--color-secondary);
}

.page-loader.page-loader-admin .page-loader-text {
  color: var(--color-secondary);
}

/* Respect a reduced-motion preference: keep the bar and keep it clearly
   active, but pulse in place instead of sweeping across. */
@media (prefers-reduced-motion: reduce) {
  .page-loader {
    transition: none;
  }
  .page-loader-bar-fill {
    animation: page-loader-pulse 1.6s ease-in-out infinite;
  }
  @keyframes page-loader-pulse {
    0%, 100% { opacity: 0.35; }
    50%      { opacity: 1; }
  }
}


/* ==========================================================================
   PAGINATION — the single shared control (app/views/partials/pagination.php)
   Used by every paged list on both the admin and student sides.

   Colour comes from --pagination-accent so one component serves both: the
   admin pages set it to their red, the student pages to the site blue. Do not
   hardcode a colour here, and do not add a page-specific .pagination override
   — that is how the three inconsistent designs this replaced came about.
   ========================================================================== */
.pagination {
  --pagination-accent: var(--color-primary, var(--color-primary));

  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  margin-top: 1.3rem;
  flex-wrap: wrap;
  /* Explicitly static: a bare `nav` rule used to make every <nav> in the app
     position:fixed, which tore this control out of the page and painted it as
     a red bar across the top of the viewport. */
  position: static;
  background: none;
  padding: 0;
  min-height: 0;
  box-shadow: none;
  border: 0;
}

.pagination .page-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 36px;
  height: 36px;
  padding: 0 0.5rem;
  border-radius: 8px;
  border: 1px solid var(--color-border, var(--color-border));
  background: var(--color-white);
  color: var(--color-ink-muted);
  font-size: var(--text-xs);
  font-weight: 600;
  text-decoration: none;
  transition: border-color var(--transition-fast, 160ms ease),
    color var(--transition-fast, 160ms ease),
    background-color var(--transition-fast, 160ms ease);
}

.pagination .page-link:hover {
  border-color: var(--pagination-accent);
  color: var(--pagination-accent);
}

.pagination .page-link.active {
  background: var(--pagination-accent);
  border-color: var(--pagination-accent);
  color: var(--badge-text);
}

.pagination .page-link.disabled {
  opacity: 0.4;
  pointer-events: none;
}

.pagination .page-ellipsis {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 36px;
  color: var(--color-ink-soft, var(--color-ink-faint));
  font-size: var(--text-xs);
}

/* Admin pages are red rather than blue. Scoped to the admin shell so the
   student side is unaffected. */
.admin-main-content .pagination,
.admin-wrapper .pagination {
  --pagination-accent: var(--color-secondary);
}

@media (max-width: 480px) {
  .pagination .page-link {
    min-width: 32px;
    height: 32px;
  }
}

/* ==========================================================================
   STAT-CARD ICON BADGES
   The small coloured square holding the icon in a dashboard stat card.
   Shared so the Applications and Applicants management screens stay visually
   identical — the same reason the pagination control above lives here.
   ========================================================================== */
.icon-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 7px;
  font-size: var(--text-xs);
  flex: 0 0 auto;
}

.icon-badge-navy   { background: rgba(0, 51, 204, 0.10);   color: var(--color-primary); }
.icon-badge-blue   { background: rgba(37, 99, 235, 0.12);  color: var(--status-info-solid); }
.icon-badge-pink   { background: rgba(219, 39, 119, 0.12); color: #be185d; }
.icon-badge-amber  { background: rgba(245, 158, 11, 0.14); color: var(--status-warning-solid); }
.icon-badge-indigo { background: rgba(99, 102, 241, 0.13); color: var(--chart-2); }
.icon-badge-green  { background: rgba(16, 185, 129, 0.13); color: #0f766e; }
.icon-badge-red    { background: rgba(203, 31, 31, 0.13);  color: var(--status-danger-solid); }
.icon-badge-muted  { background: rgba(100, 116, 139, 0.13); color: var(--color-ink-soft); }

/* ==========================================================================
   DATA TABLE CELL OVERFLOW
   Loaded on every admin page, so these apply to all record tables at once.

   The problem: one applicant with a very long name wrapped to a dozen lines
   and stretched that row far taller than every other, wrecking the table's
   scannability. Names are capped at 100 characters at registration, but 100
   characters is still ~13 lines in a narrow column, and rows already in the
   database predate that limit.
   ========================================================================== */

/* General guard: an unbroken string (a long email, a pasted URL) must break
   rather than force the column wider than the page.

   break-word, NOT anywhere. The two wrap identically once a column is too
   narrow, but they differ in how the browser computes the cell's MIN-CONTENT
   width, which is the number table-layout:auto uses to decide how wide each
   column has to be:

     word "Archive"   overflow-wrap: normal      -> min-content 56.2px
                      overflow-wrap: break-word  -> min-content 56.2px
                      overflow-wrap: anywhere    -> min-content 10.4px

   Under `anywhere` the browser believes every cell can be squeezed to a
   single CHARACTER, so it hands the Actions and Type columns' width to
   whichever column has the most prose. That is what produced "Edi t",
   "Archiv e" and "Genera l" on announcements.php - buttons and badges
   wrapping mid-word inside columns crushed to ~10px of intrinsic width.
   `break-word` still breaks a genuinely unbreakable string (the guard this
   rule exists for), but keeps min-content at the longest word, so a column
   can never be starved below the width of its own contents. */
.data-table td {
  overflow-wrap: break-word;
}

/* Clamp long values to two lines with an ellipsis. Applied to the element
   INSIDE the cell, never the <td> itself — display:-webkit-box on a table
   cell breaks table layout. Pair it with a title attribute so the full value
   is still available on hover. */
.data-table .cell-clamp {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}

/* Keeps a long name from swallowing the whole table width. Tables here are
   layout:auto, so this is an upper bound rather than a fixed size — short
   names still let their column shrink. */
.data-table .cell-name {
  max-width: 18rem;
  min-width: 8rem;
}

/* Keep sticky name columns visually part of the hovered row. Page-level
   table styles give the first cell its own opaque background and shadow so it
   can cover cells while horizontally scrolling; those layers otherwise make
   the first column look like a separate hovered panel. */
.data-table tbody tr:hover > td {
  background-color: var(--color-border);
}

.data-table tbody tr:hover > td:first-child {
  background-color: var(--color-border) !important;
  box-shadow: none !important;
}

/* Admin-wide action colours and table hover treatment.  These semantic
   classes keep edit/archive actions distinct from unrelated warning and
   secondary buttons elsewhere in the admin panel. */
.admin-main-content .btn-edit,
.admin-main-content .btn-edit:hover,
.admin-main-content .btn-edit:focus-visible {
  background: #4d4dff !important;
  color: #ffffff !important;
}

.admin-main-content .btn-archive,
.admin-main-content .btn-archive:hover,
.admin-main-content .btn-archive:focus-visible {
  background: #facc15 !important;
  color: #4d3b00 !important;
}

.admin-main-content .data-table tbody tr:hover,
.admin-main-content .data-table tbody tr:hover > td,
.admin-main-content .data-table tbody tr:hover > td:first-child {
  background-color: var(--color-surface) !important;
  box-shadow: none !important;
}
