/* ===================================================================
   KNMS PREMIUM — finishing layer
   -------------------------------------------------------------------
   Loaded after app.css site-wide. Everything here is ADDITIVE polish on
   components app.css already builds: depth, light, and motion that
   responds to the visitor.

   Theme variables for the premium landing (overrides app defaults).
   Tweak these to change the visual identity site-wide.
*/
:root {
    --knms-accent: #ffb74d;      /* warm gold */
    --knms-accent-light: #ffe6b3; /* pale gold */
    --gloss-cyan: #33d7ff;      /* cyan gloss */
    --gloss-purple: #8b3ff5;    /* purple gloss */
    --knms-heading: #f4f0ff;    /* heading gradient base */
}

/* ===================================================================
   Notes and constraints from original file retained: do not change layout
   or add large geometry changes here. This sheet adds surface, light and
   motion only.
   =================================================================== */

/* ===================================================================
   1. Surfaces — glass, edge light, and the sweep
   =================================================================== */

/* A hairline of light along the top edge. Real glass catches the light
   source above it; a flat border makes a panel look like a rectangle of
   colour. This is one pseudo-element and no repaint cost. */
.card::after,
.panel::after,
.page-card::after {
    content: '';
    position: absolute;
    inset: 0 0 auto 0;
    height: 1px;
    background: linear-gradient(90deg,
        transparent 0%,
        rgba(255, 255, 255, 0.28) 22%,
        rgba(255, 255, 255, 0.5) 50%,
        rgba(255, 255, 255, 0.28) 78%,
        transparent 100%);
    opacity: 0.55;
    pointer-events: none;
    border-radius: inherit;
    transition: opacity 0.3s ease;
}
.card, .panel, .page-card { position: relative; }
.card:hover::after,
.panel:hover::after,
.page-card:hover::after { opacity: 1; }

/* The sweep: a wide, soft diagonal highlight that crosses a card on hover.
   Distinct from the button sheen in app.css — that one is a narrow specular
   glint on a small control, this is a slow pass of light across a large
   surface. Transform-only, so it composites without repainting the card. */
.card,
.page-card,
.news-card {
    overflow: hidden;               /* contains the sweep, nothing else */
}
.card > .kn-sweep,
.page-card > .kn-sweep,
.news-card > .kn-sweep,
.card::before,
.page-card::before {
    pointer-events: none;
}
.page-card::before {
    content: '';
    position: absolute;
    top: -60%;
    left: -80%;
    width: 60%;
    height: 220%;
    z-index: 1;
    background: linear-gradient(100deg,
        transparent 0%,
        rgba(255, 255, 255, 0.06) 38%,
        rgba(255, 255, 255, 0.14) 50%,
        rgba(255, 255, 255, 0.06) 62%,
        transparent 100%);
    transform: translateX(0) skewX(-12deg);
    transition: transform 0.85s cubic-bezier(0.22, 0.7, 0.3, 1);
}
.page-card:hover::before { transform: translateX(320%) skewX(-12deg); }

/* Depth on hover. app.css already lifts these 1-2px; this adds the shadow
   that makes the lift readable, and a scale small enough to feel like
   proximity rather than a zoom. */
.page-card,
.news-card {
    transition: transform 0.35s cubic-bezier(0.22, 0.7, 0.3, 1),
                box-shadow 0.35s ease,
                border-color 0.35s ease;
}
.page-card:hover,
.news-card:hover {
    transform: translateY(-4px) scale(1.008);
    box-shadow:
        0 18px 40px -18px rgba(0, 0, 0, 0.7),
        0 0 0 1px rgba(139, 92, 246, 0.3),
        0 0 34px rgba(139, 92, 246, 0.18);
}

/* ===================================================================
   2. Headings — gradient accent and a rule that draws itself
   =================================================================== */

/* The section rule grows from the left as the section scrolls in, riding
   the .reveal class app.js already applies. No new observer, no new JS. */
.home-section > h2,
.home-section > h3 {
    position: relative;
    padding-bottom: 0.55rem;
}
.home-section > h2::after,
.home-section > h3::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: 0;
    height: 2px;
    width: 100%;
    max-width: 220px;
    border-radius: 2px;
    background: linear-gradient(90deg,
        var(--knms-accent, #ffc94a),
        var(--gloss-purple, #8b3ff5) 55%,
        transparent 100%);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 0.8s cubic-bezier(0.22, 0.7, 0.3, 1) 0.15s;
}
.home-section.is-visible > h2::after,
.home-section.is-visible > h3::after,
/* Sections that never get the reveal treatment (no JS, or not observed)
   must still show the rule — an animation that only ever runs in one
   direction should never be the only thing making a mark visible. */
.home-section:not(.reveal) > h2::after,
.home-section:not(.reveal) > h3::after { transform: scaleX(1); }

/* Page titles get the accent as a gradient in the text itself. Applied
   only to h1 inside the main column so admin table headers and card
   headings are untouched. */
.page > h1 {
    background: linear-gradient(120deg,
        var(--knms-heading, #f4f0ff) 0%,
        var(--knms-heading, #f4f0ff) 42%,
        var(--knms-accent-light, #ffe08a) 72%,
        var(--knms-accent, #ffc94a) 100%);
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
}
/* -webkit-text-fill-color INHERITS, and it beats `color` on any descendant
   that sets one. The rule above therefore made every child of an h1
   invisible — which is exactly what happened to the avatar initials inside
   `h1.name-row` on the profile page: the circle rendered, the letters did not.
   Any element nested in a gradient heading has to opt back out. */
.page > h1 * {
    -webkit-text-fill-color: currentColor;
}
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
    .page > h1 { color: var(--knms-heading, #f4f0ff); -webkit-text-fill-color: currentColor; }
}

/* ===================================================================
   3. Motion — entrance, stagger, and page arrival
   =================================================================== */

/* A softer, longer reveal than app.css's default, with a touch of scale.
   Overriding rather than adding a second animation keeps one transition on
   the property and avoids the two fighting. */
.reveal {
    transition:
        opacity 0.75s cubic-bezier(0.22, 0.7, 0.3, 1),
        transform 0.75s cubic-bezier(0.22, 0.7, 0.3, 1);
}

/* Staggered arrival for grids that app.js does not observe individually.
   Six steps then stop: past that the last card waits long enough that the
   page feels slow rather than considered. */
.page-card-grid > *,
.stat-band > * {
    animation: kn-premium-rise 0.7s cubic-bezier(0.22, 0.7, 0.3, 1) both;
}
.page-card-grid > *:nth-child(1), .stat-band > *:nth-child(1) { animation-delay: 0.04s; }
.page-card-grid > *:nth-child(2), .stat-band > *:nth-child(2) { animation-delay: 0.10s; }
.page-card-grid > *:nth-child(3), .stat-band > *:nth-child(3) { animation-delay: 0.16s; }
.page-card-grid > *:nth-child(4), .stat-band > *:nth-child(4) { animation-delay: 0.22s; }
.page-card-grid > *:nth-child(5), .stat-band > *:nth-child(5) { animation-delay: 0.28s; }
.page-card-grid > *:nth-child(n + 6), .stat-band > *:nth-child(n + 6) { animation-delay: 0.34s; }

@keyframes kn-premium-rise {
    from { opacity: 0; transform: translateY(16px) scale(0.985); }
    to   { opacity: 1; transform: none; }
}

/* Flash messages slide in and settle rather than appearing. They report the
   result of something the member just did, so arriving with a small piece of
   motion is what makes them noticed at all. */
.flash {
    animation: kn-premium-flash 0.5s cubic-bezier(0.22, 0.7, 0.3, 1) both;
    position: relative;
    overflow: hidden;
}
@keyframes kn-premium-flash {
    from { opacity: 0; transform: translateY(-10px); }
    to   { opacity: 1; transform: none; }
}
/* One pass of light across a success message — a small celebration, once. */
.flash-success::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(100deg, transparent 20%, rgba(255, 255, 255, 0.18) 50%, transparent 80%);
    transform: translateX(-100%);
    animation: kn-premium-sweep 1.4s ease-out 0.3s both;
    pointer-events: none;
}
@keyframes kn-premium-sweep {
    to { transform: translateX(100%); }
}

/* ===================================================================
   4. Navigation — glass that reacts to scroll
   =================================================================== */

.topbar {
    backdrop-filter: blur(14px) saturate(1.35);
    -webkit-backdrop-filter: blur(14px) saturate(1.35);
    transition: background 0.35s ease, box-shadow 0.35s ease, border-color 0.35s ease;
}
/* app.js sets .is-scrolled on the body; the bar tightens and lifts off the
   page once content is passing under it. */
body.is-scrolled .topbar {
    background: rgba(14, 7, 34, 0.82);
    box-shadow: 0 10px 30px -16px rgba(0, 0, 0, 0.85);
}

/* Nav links grow an underline from the centre. The active link keeps it
   permanently, so hover and state use one visual language. */
.topnav a {
    position: relative;
}
.topnav a::after {
    content: '';
    position: absolute;
    left: 50%;
    right: 50%;
    bottom: 0.1rem;
    height: 2px;
    border-radius: 2px;
    background: linear-gradient(90deg, var(--knms-accent, #ffc94a), var(--gloss-cyan, #35dcff));
    transition: left 0.28s ease, right 0.28s ease, opacity 0.28s ease;
    opacity: 0;
}
.topnav a:hover::after { left: 0.35rem; right: 0.35rem; opacity: 0.9; }
.topnav a.is-active::after { left: 0.35rem; right: 0.35rem; opacity: 1; }

/* The brand mark catches the light on hover. */
.brand:hover .brand-mark-emblem,
.brand:hover .brand-logo {
    filter: drop-shadow(0 0 12px var(--glow-gold, rgba(255, 201, 74, 0.7)));
    transform: scale(1.05) rotate(-2deg);
}
.brand-mark-emblem,
.brand-logo {
    transition: filter 0.3s ease, transform 0.3s cubic-bezier(0.22, 0.7, 0.3, 1);
}

/* ===================================================================
   5. Controls — focus, fields and links
   =================================================================== */

/* One focus treatment for the whole site. Two rings — a dark spacer then
   the accent — so it stays visible on both the dark page and the pale
   surfaces inside cards. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
    outline: 2px solid var(--knms-accent, #ffc94a);
    outline-offset: 2px;
    border-radius: 6px;
}

.form-group input,
.form-group select,
.form-group textarea {
    transition: border-color 0.22s ease, box-shadow 0.22s ease, background 0.22s ease;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
    border-color: var(--gloss-cyan, #35dcff);
    box-shadow: 0 0 0 3px rgba(53, 220, 255, 0.14), 0 0 18px rgba(53, 220, 255, 0.12);
}

/* Body links get an underline that wipes in, instead of appearing whole. */
/* .nuh-btn is in the exclusion list because this matched the light
   theme's buttons whenever one sat directly inside a paragraph — the
   underline gradient painted OVER the button's own gradient and a solid
   orange button came out flat pale yellow with white text on it. Same
   rule as elsewhere: anything button-shaped joins this list; nothing
   out-specifies it downstream. The Global homepage (.g-page) is excluded
   whole: its list links are cards and tiles, and a wiping underline along
   the foot of a card is not a text link's affordance. */
.page p > a:not(.btn):not(.kn-pbtn):not(.nuh-btn):not(:is(.g-page, .cx-page) a),
.page li > a:not(.btn):not(.kn-pbtn):not(.nuh-btn):not(:is(.g-page, .cx-page) a) {
    background-image: linear-gradient(90deg, var(--knms-accent, #ffc94a), var(--knms-accent, #ffc94a));
    background-repeat: no-repeat;
    background-position: 0 100%;
    background-size: 0% 1.5px;
    transition: background-size 0.3s ease, color 0.2s ease;
}
.page p > a:not(.btn):not(.kn-pbtn):not(.nuh-btn):not(:is(.g-page, .cx-page) a):hover,
.page li > a:not(.btn):not(.kn-pbtn):not(.nuh-btn):not(:is(.g-page, .cx-page) a):hover {
    background-size: 100% 1.5px;
}

/* ===================================================================
   6. Loading states
   =================================================================== */

/* A shimmer for anything rendered as a placeholder. Uses a moving gradient
   rather than a pulsing opacity: opacity pulses read as "broken", a
   left-to-right shimmer reads as "arriving". */
.is-skeleton,
.skeleton {
    background-image: linear-gradient(100deg,
        rgba(255, 255, 255, 0.03) 30%,
        rgba(255, 255, 255, 0.10) 48%,
        rgba(255, 255, 255, 0.03) 66%);
    background-size: 220% 100%;
    animation: kn-premium-shimmer 1.5s linear infinite;
    border-radius: 8px;
    color: transparent !important;
}
@keyframes kn-premium-shimmer {
    from { background-position: 120% 0; }
    to   { background-position: -120% 0; }
}

/* ===================================================================
   7. Scrollbar
   =================================================================== */

::-webkit-scrollbar-thumb {
    background: linear-gradient(180deg, var(--gloss-purple, #8b3ff5), var(--gloss-cyan, #35dcff));
    border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover {
    background: linear-gradient(180deg, var(--knms-accent, #ffc94a), var(--gloss-purple, #8b3ff5));
}

/* ===================================================================
   8. Motion policy
   -------------------------------------------------------------------
   Everything above is ornament. Under a reduced-motion preference it all
   stops — but note that each rule resets to the FINISHED state, never the
   starting one. An entrance animation that is merely cancelled leaves its
   element at opacity 0, which hides content from exactly the people who
   asked for less motion.
   =================================================================== */

@media (prefers-reduced-motion: reduce) {
    .page-card-grid > *,
    .stat-band > *,
    .flash {
        animation: none;
        opacity: 1;
        transform: none;
    }
    .flash-success::after,
    .is-skeleton, .skeleton { animation: none; }
    .page-card:hover,
    .news-card:hover { transform: none; }
    .page-card::before { transition: none; }
    .home-section > h2::after,
    .home-section > h3::after { transform: scaleX(1); transition: none; }
    .topnav a::after { transition: none; }
    .brand-mark-emblem, .brand-logo { transition: none; }
    .page p > a:not(.btn):not(.kn-pbtn):not(.nuh-btn):not(:is(.g-page, .cx-page) a),
    .page li > a:not(.btn):not(.kn-pbtn):not(.nuh-btn):not(:is(.g-page, .cx-page) a) { transition: none; }
}

/* ===================================================================
   Pincode autofill UI polish
   =================================================================== */
.pincode-suggestions {
    position: absolute;
    z-index: 9999;
    background: #ffffff;
    border: 1px solid rgba(13,31,56,0.06);
    box-shadow: 0 10px 30px rgba(10,10,20,0.06);
    border-radius: 8px;
    padding: 6px 0;
    font-size: 0.95rem;
    color: var(--nuh-ink);
    max-height: 320px;
    overflow: auto;
}
.pincode-suggestion {
    padding: 0.45rem 0.8rem;
    cursor: pointer;
    white-space: nowrap;
}
.pincode-suggestion:hover,
.pincode-suggestion:focus,
.pincode-suggestion.is-active {
    background: rgba(31,157,99,0.06);
    outline: none;
}
input.is-autofilled {
    background: linear-gradient(90deg, rgba(31,157,99,0.04), rgba(0,0,0,0)) !important;
}
input.is-loading { position: relative; }
input.is-loading::after {
    content: '';
    display: inline-block;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    box-sizing: border-box;
    border: 2px solid rgba(0,0,0,0.12);
    border-top-color: rgba(0,0,0,0.44);
    margin-left: 8px;
    vertical-align: middle;
    animation: kn-spin 0.8s linear infinite;
}
@keyframes kn-spin { to { transform: rotate(360deg); } }

/* ===================================================================
   Landing enhancements: hero polish, cards, showcase carousel, reveals
   =================================================================== */
   .home-section.reveal { opacity: 1; transform: none; }.home-section { opacity: 0; transform: translateY(14px); transition: opacity 0.85s cubic-bezier(0.22,0.7,0.3,1), transform 0.85s cubic-bezier(0.22,0.7,0.3,1); }

.stat-band { display:flex; gap:1rem; justify-content:center; align-items:center; padding:1.2rem 0; }
.stat-band-item { text-align:center; padding:0.6rem 1.2rem; background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0)); border-radius:10px; min-width:110px; }
.stat-band-item .value { font-weight:700; font-size:1.2rem; display:block; color:var(--knms-accent); }
.stat-band-item .label { display:block; color:var(--knms-muted); font-size:0.9rem; }

.showcase-strip { display:flex; gap:12px; overflow:hidden; align-items:center; }
.showcase-item { flex:0 0 auto; width:220px; height:140px; border-radius:10px; overflow:hidden; position:relative; box-shadow: 0 10px 30px rgba(10,10,20,0.06); transition: transform 300ms ease, box-shadow 300ms ease; }
.showcase-item img { width:100%; height:100%; object-fit:cover; display:block; }
.showcase-item:hover { transform: translateY(-6px) scale(1.02); box-shadow: 0 18px 40px rgba(10,10,20,0.12); }

.news-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:1rem; }
.news-card { display:block; border-radius:10px; overflow:hidden; background:var(--knms-surface); transition: transform 220ms ease, box-shadow 220ms ease; }
.news-card.is-featured { grid-column: span 2; }
.news-card img.news-card-media { width:100%; height:140px; object-fit:cover; display:block; }
.news-card .news-card-body { padding:0.8rem; display:block; }
.news-card.is-hover { transform: translateY(-6px); box-shadow: 0 18px 40px rgba(10,10,20,0.08); }

.mk-grid-cards { display:grid; gap:1rem; grid-template-columns: repeat(auto-fit, minmax(220px,1fr)); }
.mk-card { padding:0.9rem; border-radius:10px; background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0)); transition: transform 220ms ease, box-shadow 220ms ease; text-decoration:none; color:inherit; display:block; }
.mk-card.is-hover, .mk-card:hover { transform: translateY(-6px); box-shadow: 0 18px 40px rgba(10,10,20,0.06); }
.mk-card img.mk-card__img { width:100%; height:140px; object-fit:cover; border-radius:8px; display:block; margin-bottom:0.6rem; }

.kn-btn { transition: transform 220ms cubic-bezier(0.22,0.7,0.3,1), box-shadow 220ms ease; }
.kn-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(10,10,20,0.08); }

/* small screens: make showcase narrower */
@media (max-width:640px) {
    .showcase-item { width:160px; height:120px; }
    .stat-band { flex-wrap:wrap; }
}

/* ===================================================================
   9. The premium switch — reveal choreography
   -------------------------------------------------------------------
   Everything from here down is the deep premium layer, and all of it
   answers to body[data-premium="1"] — the `premium_effects` site
   setting, stamped onto <body> by header.php. Turned off, the admin
   gets a plain, fast-rendering page; the base sheet above predates the
   switch and stays as it was.

   .reveal-pending / .reveal-in are toggled by the reveal script. The
   hidden state is DOUBLY guarded: it only exists under
   data-premium="1", and the script only applies the class when JS is
   actually running and the visitor has not asked for reduced motion.
   No-JS and reduced-motion visitors never meet a hidden element — their
   worst case is content that was simply always visible.
   =================================================================== */

body[data-premium="1"] .reveal-pending {
    opacity: 0;
    transform: translateY(14px);
}
.reveal-in {
    opacity: 1;
    transform: none;
    transition: opacity 0.5s ease, transform 0.6s cubic-bezier(0.2, 0.7, 0.3, 1);
}

/* ===================================================================
   10. CMS blocks — the established set
   -------------------------------------------------------------------
   app.css builds the timeline, pull quote, stat tiles and CTA panel;
   this deepens their light. Same rule as ever: surface and motion
   only, nothing that moves layout.
   =================================================================== */

/* Timeline. The rail already carries the three-colour gradient; here
   it gains a soft halo and fades out at its tail instead of stopping
   dead. Markers answer the pointer — proximity, not decoration. */
body[data-premium="1"] .timeline::before {
    box-shadow: 0 0 10px var(--glow-purple, rgba(139, 92, 246, 0.5)),
                0 0 2px rgba(255, 255, 255, 0.3);
    -webkit-mask-image: linear-gradient(180deg, #000 88%, transparent 100%);
            mask-image: linear-gradient(180deg, #000 88%, transparent 100%);
}
body[data-premium="1"] .timeline-marker {
    transition: transform 0.3s cubic-bezier(0.22, 0.7, 0.3, 1), box-shadow 0.3s ease;
}
body[data-premium="1"] .timeline-item:hover .timeline-marker {
    transform: scale(1.25);
    box-shadow: 0 0 0 3px var(--knms-surface, #23134a),
                0 0 12px var(--glow-gold, rgba(255, 185, 46, 0.75)),
                0 0 20px var(--glow-purple, rgba(139, 92, 246, 0.7));
}
body[data-premium="1"] .timeline-item:hover .timeline-year {
    text-shadow: 0 0 10px var(--glow-purple, rgba(139, 92, 246, 0.7));
}

/* Pull quote. The mark grows into a true display glyph, and the left
   edge gets a lit accent. The edge REPLACES the .card::after top
   hairline on this one component — same pseudo-element, later rule —
   because a quote's light source is its accent side, not its top. All
   longhands the hairline set are restated, so nothing leaks through. */
body[data-premium="1"] .cms-quote::before {
    font-size: 7.5rem;
    top: -1.6rem;
    opacity: 0.16;
}
body[data-premium="1"] .cms-quote::after {
    content: '';
    position: absolute;
    inset: 0 auto 0 0;
    height: auto;
    width: 3px;
    border-radius: 3px;
    opacity: 1;
    background: linear-gradient(180deg,
        var(--knms-accent, #ffb92e),
        var(--gloss-purple, #8b5cf6) 62%,
        transparent 100%);
    pointer-events: none;
}

/* Stat tiles. The number is the content; give it ink worth reading —
   gold poured top to bottom, glow moved BEHIND the glyphs with a
   drop-shadow filter (text-shadow shows straight through gradient-
   clipped text and muddies it). tabular-nums so the count-up in the
   new stat blocks ticks without the digits shuffling sideways.
   .stat-value is the cms-blocks.css hook, [data-countup] included. */
body[data-premium="1"] .stat-tile .value,
body[data-premium="1"] .stat-value {
    font-variant-numeric: tabular-nums;
    background-image: linear-gradient(180deg,
        var(--knms-accent-light, #ffd77a) 0%,
        var(--knms-accent, #ffb92e) 60%,
        var(--knms-accent-deep, #c88516) 115%);
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
    text-shadow: none;
    filter: drop-shadow(0 0 12px rgba(255, 185, 46, 0.24));
}
/* Same inheritance trap as .page > h1 in section 2: anything nested
   inside a clipped value opts back in to its own colour. */
body[data-premium="1"] .stat-tile .value *,
body[data-premium="1"] .stat-value * {
    -webkit-text-fill-color: currentColor;
}
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
    body[data-premium="1"] .stat-tile .value,
    body[data-premium="1"] .stat-value {
        background-image: none;
        color: var(--knms-accent, #ffb92e);
        -webkit-text-fill-color: currentColor;
        filter: none;
    }
}

/* CTA panel. A slow wash of warm and violet light drifts behind the
   copy — overscanned insets so its soft edges never show. ::before
   paints under the panel's children by tree order, so no z-index
   bookkeeping. The button inside already sweeps (app.css .btn::after);
   on the one panel whose whole job is the click, the glint brightens. */
body[data-premium="1"] .cms-cta {
    overflow: hidden;
}
body[data-premium="1"] .cms-cta::before {
    content: '';
    position: absolute;
    inset: -45%;
    background:
        radial-gradient(closest-side at 30% 35%, rgba(255, 185, 46, 0.16), transparent 72%),
        radial-gradient(closest-side at 72% 68%, rgba(139, 92, 246, 0.22), transparent 72%);
    animation: kn-premium-drift 16s ease-in-out infinite alternate;
    pointer-events: none;
}
@keyframes kn-premium-drift {
    from { transform: translate3d(-3%, -2%, 0) rotate(0deg); }
    to   { transform: translate3d(3%, 2%, 0) rotate(3deg); }
}
body[data-premium="1"] .cms-cta .btn::after {
    background: linear-gradient(120deg, transparent, rgba(255, 255, 255, 0.85), transparent);
}

/* ===================================================================
   11. CMS blocks — the new set
   -------------------------------------------------------------------
   cms-blocks.css owns these components' geometry; this layer adds the
   same depth, light and response the established blocks get. Styled by
   class contract, so the two sheets stay independent.
   =================================================================== */

/* FAQ — native <details>. The chevron is drawn here (borders, no
   glyph) and turns over as the item opens; the answer arrives with a
   short fade so opening feels like unfolding, not switching. */
body[data-premium="1"] .cms-faq-item > summary {
    position: relative;
    list-style: none;
    cursor: pointer;
    transition: background-color 0.25s ease;
}
body[data-premium="1"] .cms-faq-item > summary::-webkit-details-marker { display: none; }
body[data-premium="1"] .cms-faq-item > summary:hover {
    background-color: rgba(255, 255, 255, 0.045);
}
body[data-premium="1"] .cms-faq-item > summary::after {
    content: '';
    position: absolute;
    right: 1rem;
    top: 50%;
    width: 0.5em;
    height: 0.5em;
    border-right: 2px solid var(--knms-accent, #ffb92e);
    border-bottom: 2px solid var(--knms-accent, #ffb92e);
    border-radius: 1px;
    transform: translateY(-70%) rotate(45deg);
    transition: transform 0.35s cubic-bezier(0.22, 0.7, 0.3, 1);
}
body[data-premium="1"] .cms-faq-item[open] > summary::after {
    transform: translateY(-30%) rotate(225deg);
}
body[data-premium="1"] .cms-faq-item[open] > *:not(summary) {
    animation: kn-premium-unfold 0.4s cubic-bezier(0.22, 0.7, 0.3, 1);
}
@keyframes kn-premium-unfold {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: none; }
}

/* Testimonials. Each voice gets a card's depth and the quote mark the
   pull-quote wears, smaller — one visual language for quoted speech.
   The carousel strip glides rather than jumping when its controls page
   it along. */
body[data-premium="1"] .cms-testimonials[data-carousel] {
    scroll-behavior: smooth;
}
body[data-premium="1"] .cms-testimonial {
    position: relative;
    box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.4));
    transition: transform 0.35s cubic-bezier(0.22, 0.7, 0.3, 1), box-shadow 0.35s ease;
}
body[data-premium="1"] .cms-testimonial::before {
    content: '\201C';
    position: absolute;
    top: 0;
    left: 0.6rem;
    font-family: 'Baloo 2', Georgia, serif;
    font-size: 3.4rem;
    line-height: 1.1;
    color: var(--gloss-purple, #8b5cf6);
    opacity: 0.2;
    pointer-events: none;
}
body[data-premium="1"] .cms-testimonial:hover {
    transform: translateY(-3px);
    box-shadow: 0 16px 34px -16px rgba(0, 0, 0, 0.65),
                0 0 0 1px rgba(139, 92, 246, 0.25);
}

/* Feature grid. Cells lift; the leading icon sits in a lit chip with
   an accent ring — ring and wash are box-shadow and background only,
   no padding or size change, so whatever box the skeleton gave the
   icon keeps its geometry. */
body[data-premium="1"] .cms-feature-grid > * {
    transition: transform 0.3s cubic-bezier(0.22, 0.7, 0.3, 1), box-shadow 0.3s ease;
}
body[data-premium="1"] .cms-feature-grid > *:hover {
    transform: translateY(-3px);
    box-shadow: 0 14px 30px -14px rgba(0, 0, 0, 0.6),
                0 0 0 1px rgba(139, 92, 246, 0.22);
}
body[data-premium="1"] .cms-feature-grid .cms-feature-icon,
body[data-premium="1"] .cms-feature-grid > * > svg:first-child,
body[data-premium="1"] .cms-feature-grid > * > img:first-child {
    border-radius: 12px;
    background: radial-gradient(circle at 30% 25%,
        rgba(255, 185, 46, 0.16),
        rgba(139, 92, 246, 0.10) 75%);
    box-shadow: 0 0 0 1px rgba(255, 185, 46, 0.35),
                0 0 14px rgba(255, 185, 46, 0.12);
    transition: box-shadow 0.3s ease;
}
body[data-premium="1"] .cms-feature-grid > *:hover .cms-feature-icon,
body[data-premium="1"] .cms-feature-grid > *:hover > svg:first-child,
body[data-premium="1"] .cms-feature-grid > *:hover > img:first-child {
    box-shadow: 0 0 0 1px var(--knms-accent, #ffb92e),
                0 0 20px rgba(255, 185, 46, 0.28);
}

/* Gallery. The zoom happens INSIDE the frame: the item clips, the
   image scales, the grid never moves. focus-within included so a
   keyboard visitor tabbing through linked photos gets the same
   response as the pointer. */
body[data-premium="1"] .cms-gallery-item {
    position: relative;
    overflow: hidden;
}
body[data-premium="1"] .cms-gallery-item img {
    transition: transform 0.6s cubic-bezier(0.22, 0.7, 0.3, 1);
}
body[data-premium="1"] .cms-gallery-item:hover img,
body[data-premium="1"] .cms-gallery-item:focus-within img {
    transform: scale(1.06);
}
body[data-premium="1"] .cms-gallery-item::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: linear-gradient(180deg, transparent 62%, rgba(0, 0, 0, 0.35) 100%);
    opacity: 0;
    transition: opacity 0.4s ease;
    pointer-events: none;
}
body[data-premium="1"] .cms-gallery-item:hover::after,
body[data-premium="1"] .cms-gallery-item:focus-within::after {
    opacity: 1;
}

/* Countdown. Tabular digits so nothing shuffles as seconds fall, a low
   gold haze over the figures, and separators that breathe on a slow
   two-second cycle — the block's pulse, not a distraction. Both likely
   separator hooks are covered; an unmatched selector costs nothing. */
body[data-premium="1"] .cms-countdown {
    font-variant-numeric: tabular-nums;
    text-shadow: 0 0 14px rgba(255, 185, 46, 0.14);
}
body[data-premium="1"] .cms-countdown .cms-countdown-sep,
body[data-premium="1"] .cms-countdown .sep {
    color: var(--knms-accent, #ffb92e);
    text-shadow: 0 0 12px var(--glow-gold, rgba(255, 185, 46, 0.75));
    animation: kn-premium-tick 2s ease-in-out infinite;
}
@keyframes kn-premium-tick {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.45; }
}

/* ===================================================================
   12. Micro-interactions, site-wide
   =================================================================== */

/* Generic cards join the lift the page-cards already have — shallower,
   because a form card is furniture, not an invitation. page/news cards
   keep their own deeper treatment from section 1, hence the guards. */
body[data-premium="1"] .card:not(.page-card):not(.news-card) {
    transition: transform 0.3s cubic-bezier(0.22, 0.7, 0.3, 1),
                box-shadow 0.3s ease,
                border-color 0.3s ease;
}
body[data-premium="1"] .card:not(.page-card):not(.news-card):hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow-md, 0 6px 20px rgba(0, 0, 0, 0.5)),
                0 0 0 1px rgba(139, 92, 246, 0.34),
                0 0 28px rgba(139, 92, 246, 0.22);
}

/* The landing addendum's .kn-btn lifts but never had the glint the
   .btn and .nuh-btn families carry. Same idiom as both: an overlay
   parked off the left edge that crosses on invitation only — no
   geometry change. */
body[data-premium="1"] .kn-btn {
    position: relative;
    overflow: hidden;
}
body[data-premium="1"] .kn-btn::after {
    content: '';
    position: absolute;
    top: -10%;
    bottom: -10%;
    left: -45%;
    width: 36%;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.45), transparent);
    transform: skewX(-18deg) translateX(0);
    transition: transform 0.55s ease;
    pointer-events: none;
}
body[data-premium="1"] .kn-btn:hover::after,
body[data-premium="1"] .kn-btn:focus-visible::after {
    transform: skewX(-18deg) translateX(430%);
}

/* The section-5 focus ring stays on regardless of the switch — focus
   visibility is accessibility, not ornament. Premium adds only a soft
   halo around it. */
body[data-premium="1"] a:focus-visible,
body[data-premium="1"] button:focus-visible,
body[data-premium="1"] input:focus-visible,
body[data-premium="1"] select:focus-visible,
body[data-premium="1"] textarea:focus-visible,
body[data-premium="1"] [tabindex]:focus-visible {
    box-shadow: 0 0 0 5px rgba(255, 185, 46, 0.16);
}

/* Anchor scrolls glide instead of jumping — only when the visitor
   allows motion, and only while the premium layer is on. :has() lets
   the body attribute reach the scrolling element; a browser without it
   simply keeps instant jumps. */
@media (prefers-reduced-motion: no-preference) {
    html:has(body[data-premium="1"]) {
        scroll-behavior: smooth;
    }
}

/* Selection in the house gold, dark ink on it — readable on the dark
   theme and the light one alike. app.css's purple selection stands
   when the layer is off. */
body[data-premium="1"] ::selection {
    background: var(--knms-accent, #ffb92e);
    color: #241303;
    text-shadow: none;
}

/* ===================================================================
   13. Typography — gradient ink in the light sections
   -------------------------------------------------------------------
   Headings inside .nuh-section get gradient ink: the run starts from
   currentColor — NOT a fixed colour — and tips into the brand orange
   past its end. currentColor is the whole trick: a navy .nuh-h2 pours
   navy-into-orange, and a white heading on one of the dark promo bands
   pours white-into-orange, so no band can vanish its own heading (the
   exact accident the body.nuh-page scoping in unityhub.css fixed
   once already). `color` itself is left untouched — only the fill is
   clipped — so currentColor keeps resolving, admin re-theming keeps
   working, and every descendant span keeps its own ink via the
   opt-out below. Screen only: print keeps solid ink.
   =================================================================== */

@media screen {
    body[data-premium="1"] .nuh-section h1,
    body[data-premium="1"] .nuh-section h2 {
        background-image: linear-gradient(115deg,
            currentColor 0%,
            currentColor 62%,
            var(--nuh-orange, #f7941e) 135%);
        -webkit-background-clip: text;
                background-clip: text;
        -webkit-text-fill-color: transparent;
    }
    body[data-premium="1"] .nuh-section h1 *,
    body[data-premium="1"] .nuh-section h2 * {
        -webkit-text-fill-color: currentColor;
    }
    /* No clip support, no clipped text — solid ink, exactly as before. */
    @supports not ((-webkit-background-clip: text) or (background-clip: text)) {
        body[data-premium="1"] .nuh-section h1,
        body[data-premium="1"] .nuh-section h2 {
            background-image: none;
            -webkit-text-fill-color: currentColor;
        }
    }
    /* High-contrast mode wants flat, maximum-contrast ink. a11y.css
       cannot know about selectors added here, so this sheet retires
       its own gradient under that flag itself. */
    html[data-contrast="high"] body[data-premium="1"] .nuh-section h1,
    html[data-contrast="high"] body[data-premium="1"] .nuh-section h2 {
        background-image: none;
        -webkit-background-clip: border-box;
                background-clip: border-box;
        -webkit-text-fill-color: currentColor;
    }
}

/* .nuh-h2 sets family, size and colour but never tracking; display
   sizes read better fractionally tight. (Checked: neither app.css nor
   unityhub.css sets letter-spacing on it — this adds, not fights.) */
body[data-premium="1"] .nuh-h2 {
    letter-spacing: -0.015em;
}

/* ===================================================================
   14. Motion policy for the premium layer
   -------------------------------------------------------------------
   Same contract as section 8: under reduced motion everything lands in
   its FINISHED state. A cancelled entrance that leaves opacity at 0
   hides content from exactly the people who asked for calm.
   =================================================================== */

@media (prefers-reduced-motion: reduce) {
    /* The reveal script never applies .reveal-pending under this
       preference; restated for a cached page that still carries it. */
    body[data-premium="1"] .reveal-pending {
        opacity: 1;
        transform: none;
    }
    .reveal-in { transition: none; }

    body[data-premium="1"] .cms-cta::before { animation: none; }
    body[data-premium="1"] .cms-faq-item[open] > *:not(summary) { animation: none; }
    body[data-premium="1"] .cms-countdown .cms-countdown-sep,
    body[data-premium="1"] .cms-countdown .sep { animation: none; opacity: 1; }
    body[data-premium="1"] .cms-testimonials[data-carousel] { scroll-behavior: auto; }

    body[data-premium="1"] .timeline-marker,
    body[data-premium="1"] .cms-faq-item > summary,
    body[data-premium="1"] .cms-faq-item > summary::after,
    body[data-premium="1"] .cms-testimonial,
    body[data-premium="1"] .cms-feature-grid > *,
    body[data-premium="1"] .cms-feature-grid .cms-feature-icon,
    body[data-premium="1"] .cms-feature-grid > * > svg:first-child,
    body[data-premium="1"] .cms-feature-grid > * > img:first-child,
    body[data-premium="1"] .cms-gallery-item img,
    body[data-premium="1"] .cms-gallery-item::after,
    body[data-premium="1"] .card:not(.page-card):not(.news-card) { transition: none; }

    body[data-premium="1"] .timeline-item:hover .timeline-marker,
    body[data-premium="1"] .cms-testimonial:hover,
    body[data-premium="1"] .cms-feature-grid > *:hover,
    body[data-premium="1"] .cms-gallery-item:hover img,
    body[data-premium="1"] .cms-gallery-item:focus-within img,
    body[data-premium="1"] .card:not(.page-card):not(.news-card):hover { transform: none; }

    /* The glint holds still, exactly as .nuh-btn's does under this
       preference in unityhub.css. Smooth anchor scrolling needs no
       undoing — it lives inside the no-preference query above. */
    body[data-premium="1"] .kn-btn::after { display: none; }
}

/* Touch screens: hover is a tap here, and a tapped card that stays
   lifted looks stuck. Mirrors app.css's own (hover: none) guard, which
   these higher-specificity rules would otherwise override. */
@media (hover: none) {
    body[data-premium="1"] .card:not(.page-card):not(.news-card):hover,
    body[data-premium="1"] .cms-testimonial:hover,
    body[data-premium="1"] .cms-feature-grid > *:hover { transform: none; }
    body[data-premium="1"] .cms-gallery-item:hover img { transform: none; }
}

/* ===================================================================
   15. Light-theme correction — hero headings on public pages
   -------------------------------------------------------------------
   app.css's headline gradient (white → cyan → purple, its line ~1342)
   was drawn for the dark console theme, and it leaks onto the light
   public theme: a CMS page hero renders its title in near-white ink on
   a white card. The public theme is marked by body.nuh-page, so the
   correction scopes there — same properties, later in the cascade, no
   !important — and restates the gradient in ink that belongs to the
   light ground: the page's own heading colour warming into the brand
   orange. Deliberately NOT behind the premium toggle: a readable title
   is correctness, not ornament.
   =================================================================== */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
    body.nuh-page .dashboard-header h1,
    body.nuh-page .hero h1 {
        background: linear-gradient(100deg,
            var(--nuh-blue-deep, #113769) 30%,
            var(--nuh-blue, #1d4e8f) 62%,
            var(--nuh-orange, #e8944a) 100%);
        -webkit-background-clip: text;
        background-clip: text;
        -webkit-text-fill-color: transparent;
        color: transparent;
    }
}

/* ===================================================================
   16. Mobile legibility corrections
   -------------------------------------------------------------------
   Found by a 360px audit of the pages built most recently. Nothing here
   is a layout change: the audit found no horizontal overflow, no table
   escaping its wrapper and no element hidden behind the bottom bar. What
   it did find was TEXT TOO SMALL TO READ ON A PHONE — the badge pills
   that carry the hub filters, the state names in the browse-by-state
   list, and the labels under the bottom navigation, all rendering
   between 10.9px and 11.8px.

   11.5px is defensible on a desktop pill the reader is not squinting at.
   On a phone, held at arm's length, it is the difference between a
   filter somebody uses and one they ignore — and the state browser is
   now the main way anyone reaches 441 hubs.

   Scoped to coarse pointers so the desktop rendering, which was designed
   at these sizes, is untouched. Sizes rise to 12px, the floor the audit
   checks, and no further: these are labels, not body copy, and growing
   them more would start wrapping chips that currently fit.
   =================================================================== */
@media (pointer: coarse), (max-width: 600px) {
    .badge-pill,
    .hub-card__cat,
    .hub-card__near,
    .biz-verified {
        font-size: 0.75rem;   /* 12px */
    }
    /* The count inside a state chip. It was set to 0.72rem here on the
       theory that a secondary number may be smaller — the audit
       disagreed, and it was right: 11.5px inside a chip somebody is
       trying to read at arm's length is the same problem this section
       exists to fix. It is quieter than the state name by COLOUR, which
       cell-sub already does, not by size. */
    .badge-pill .cell-sub {
        font-size: 0.75rem;
    }

    /* These two live in unityhub.css, which loads AFTER this sheet, so a
       bare class selector here loses the cascade on every public page.
       The theme's own body class is the cheapest way to outrank it
       without !important. */
    body.nuh-page .hub-card__cat,
    body.nuh-page .hub-card__near,
    body.nuh-page .biz-verified,
    body.nuh-page .eyebrow {
        font-size: 0.75rem;
    }
    /* The bottom bar's labels were the smallest text on the site at
       10.88px, on the control a phone user touches most. */
    .footer-nav .footer-link {
        font-size: 0.75rem;
    }

    /* Gallery captions, table headers and the section eyebrow — the last
       sub-12px labels on the newest pages. The table headers matter most:
       an events table on a phone is already dense, and a column heading
       nobody can read makes the row beneath it guesswork. */
    .gallery-item-caption,
    .cms-gallery-item figcaption,
    .eyebrow,
    table th {
        font-size: 0.75rem;
    }
}

/* ===================================================================
   17. Quick-add — the raised "+" in the bottom bar
   -------------------------------------------------------------------
   The client's mobile artwork carries a bottom bar of five items whose
   CENTRE item is a raised circular button: the one-tap route to
   everything a member is allowed to create. This section is that button
   and the sheet of actions it opens. includes/footer.php builds the
   actions and filters them against the gates the destination pages
   enforce; this file only has to make them look like somewhere worth
   tapping.

   Three decisions worth writing down, because each one is the answer to
   something that went wrong the obvious way round:

   1. THE "+" IS NOT A FLEX ITEM. The bar is `display: flex` with five
      `flex: 1` tabs, and at 360px "Community" is already clamped at its
      own min-content width (75px) while its neighbours share what is
      left. Give the button a sixth slot and every label loses about
      12px — on the control a phone user touches more than any other. So
      the whole quick-add is `position: absolute` over the bar instead:
      the five tabs keep the exact geometry they have today, and the
      circle lands on the true horizontal centre rather than wherever a
      sixth flex slot happened to fall (which, with five tabs, is never
      the middle — five does not split in half).

   2. IT DIPS 8px INTO THE BAR AND NO FURTHER. A raised centre button
      that overlaps its bar by half looks right in a mock-up and eats the
      tab underneath it in a browser. The tabs are 52px tall, the floor
      for a tap target is 44px, and 52 - 44 = 8. So eight pixels is the
      entire overlap budget: enough for the circle to read as attached to
      the bar, and not one pixel out of anybody's target.

   3. THE PAGE GAINS BOTTOM PADDING. app.css reserves --footer-h (72px)
      under every page for the fixed bar, which clears the bar but not a
      56px circle standing on top of it. Without the extra room the last
      control on a page — "Save", usually — can end up under the "+".

   Everything is inside the coarse/narrow query, so a desktop mouse
   pointer sees the bar exactly as it was: the quick-add is
   `display: none` there and takes no space, no slot and no tab stop.
   =================================================================== */

/* Desktop and any fine pointer: not present at all. This bar is fixed at
   every width on this site, so without this the <details> would render as
   a stray disclosure triangle in the middle of a 1440px footer. */
.qa-fab { display: none; }

@media (max-width: 900px), (pointer: coarse) {

    /* Clearance for the circle. --footer-h alone clears the BAR; the
       circle stands 45px proud of it, so 2rem goes on top and the last
       interactive element of a page can never end up underneath it. */
    body {
        padding-bottom: calc(var(--footer-h) + 2rem + env(safe-area-inset-bottom, 0px));
    }

    /* The quick-add occupies the bar's whole box but is transparent to
       the pointer, so the five tabs underneath keep every pixel of their
       hit areas. Only the circle, the scrim and the sheet take taps.
       z-index 3 puts the group above .footer-link, which app.css makes
       `position: relative` — without it the sheet would paint under the
       News and Account tabs, which follow it in source order. */
    .qa-fab {
        display: block;
        position: absolute;
        inset: 0;
        z-index: 3;
        pointer-events: none;
    }

    /* The button. `list-style: none` plus the two marker rules removes
       the disclosure triangle; without all three, one engine or another
       draws it inside the circle. */
    .qa-fab__btn {
        position: absolute;
        left: 50%;
        bottom: 2.75rem;            /* 44px — see decision 2 above */
        transform: translateX(-50%);
        display: flex;
        align-items: center;
        justify-content: center;
        width: 3.5rem;
        height: 3.5rem;
        border-radius: 50%;
        pointer-events: auto;
        cursor: pointer;
        list-style: none;
        /* Orange, not the artwork's blue. Orange is this site's action
           colour (see the palette note at the top of unityhub.css) and it
           is the only one of the two that reads on all three bars this
           button appears over: the dark violet of the app theme, the deep
           blue of the public theme, and the white the console repaints it
           below 900px. A blue circle on the deep-blue bar would have been
           a shape you had to go looking for.
           --nuh-orange only exists under body.nuh-page, so the literal is
           the fallback for everywhere else rather than a duplicate. */
        background: linear-gradient(150deg,
            var(--nuh-orange-soft, #ff9a4d) 0%,
            var(--nuh-orange, #f47b20) 55%,
            var(--nuh-orange-ink, #c2530a) 100%);
        color: #fff;
        /* A white collar and two shadows. The collar is the load-bearing
           one: three-quarters of this circle stands over PAGE content,
           not over the bar, and the page under it is a different colour
           on every screen — a white card on the hub directory, a photo on
           the news list, an orange map on the landing. A ring tinted to
           any one of those disappears against the others, and the hub
           directory proved it: an orange circle sitting on an orange map
           of India, with a translucent white ring nobody could see.
           A near-opaque white ring reads against all of them and reads
           against both dark bars; where the page behind it is itself
           white, the neutral drop shadow underneath is what separates
           them. The warm shadow is last and is the lift, not the edge. */
        box-shadow:
            0 0 0 3px rgba(255, 255, 255, 0.92),
            0 3px 8px -1px rgba(12, 8, 30, 0.4),
            0 12px 26px -8px rgba(196, 83, 10, 0.7);
        transition: transform 0.18s cubic-bezier(0.34, 1.4, 0.64, 1),
                    box-shadow 0.18s ease;
    }
    .qa-fab__btn::-webkit-details-marker { display: none; }
    .qa-fab__btn::marker { content: ""; }

    .qa-fab__glyph {
        width: 1.75rem;
        height: 1.75rem;
        stroke-width: 2.4;
        transition: transform 0.22s cubic-bezier(0.34, 1.4, 0.64, 1);
    }

    /* Open, the "+" becomes an "x" by rotating 45 degrees. One transform
       on one element, and it says "this closes again" more plainly than a
       second icon swapped in would. */
    .qa-fab[open] .qa-fab__glyph { transform: rotate(45deg); }
    .qa-fab[open] .qa-fab__btn {
        box-shadow:
            0 0 0 3px rgba(255, 255, 255, 0.98),
            0 3px 8px -1px rgba(12, 8, 30, 0.5),
            0 16px 34px -8px rgba(196, 83, 10, 0.8);
    }
    .qa-fab__btn:active { transform: translateX(-50%) scale(0.93); }
    .qa-fab__btn:focus-visible {
        outline: 3px solid var(--nuh-orange-soft, #ff9a4d);
        outline-offset: 3px;
    }

    /* The label the circle has instead of visible text. Clipped rather
       than display:none, so it still names the control for a screen
       reader and for voice control. */
    .qa-fab__sr {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    /* The scrim. Anchored to the bar's top edge and 220vh tall, which
       covers the viewport above it on any phone; it deliberately stops at
       the bar so the five tabs stay lit and legible while the sheet is
       open. `left/right: 0` rather than 100vw on purpose — a vw unit
       counts the scrollbar and would push the document several pixels
       wider on a desktop that happens to report a coarse pointer.
       z-index -1 keeps it under the circle and the sheet, both of which
       are positioned inside this element's stacking context. */
    .qa-fab__scrim {
        position: absolute;
        left: 0;
        right: 0;
        bottom: 100%;
        height: 220vh;
        z-index: -1;
        pointer-events: auto;
        background: rgba(9, 12, 30, 0.5);
        -webkit-backdrop-filter: blur(2px);
        backdrop-filter: blur(2px);
        animation: qa-fab-fade 0.18s ease-out;
    }

    /* The sheet: one rounded card floating clear of the circle. Its
       bottom is the bar (100%) plus the circle's own reach, so it never
       lands on the "+" however tall the bar is rendered — the a11y text
       control in the site footer can make it taller. */
    .qa-fab__sheet {
        position: absolute;
        left: 0.75rem;
        right: 0.75rem;
        bottom: calc(100% + 3.75rem);
        max-height: calc(100vh - 12rem);
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
        padding: 0.4rem;
        border-radius: 18px;
        pointer-events: auto;
        background: var(--knms-surface, #23134a);
        border: 1px solid var(--knms-border, #4a2f8f);
        box-shadow:
            0 1px 0 rgba(255, 255, 255, 0.08) inset,
            0 24px 48px -12px rgba(6, 8, 24, 0.7);
        animation: qa-fab-rise 0.2s cubic-bezier(0.32, 0.72, 0.28, 1);
    }

    .qa-fab__head {
        margin: 0.35rem 0.6rem 0.5rem;
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.04em;
        text-transform: uppercase;
        color: var(--knms-muted, #b7a8e0);
    }

    /* One action per row. 2.75rem of min-height is the 44px floor; with
       the padding these land at about 48px, which is what the rest of the
       site's touch controls measure. */
    .qa-fab__act {
        display: flex;
        align-items: center;
        gap: 0.7rem;
        min-height: 2.75rem;
        padding: 0.55rem 0.6rem;
        border-radius: 12px;
        font-size: 0.9rem;
        font-weight: 600;
        color: var(--knms-text, #ece5ff);
        transition: background-color 0.14s ease, transform 0.14s ease;
    }
    .qa-fab__act:hover,
    .qa-fab__act:focus-visible {
        background: rgba(255, 255, 255, 0.08);
        color: var(--knms-text, #ece5ff);
        text-decoration: none;
    }
    .qa-fab__act:active { transform: scale(0.985); }

    /* The icon tile. A soft orange wash rather than a second solid
       circle: the "+" is the only saturated thing in this component, and
       seven more of them would flatten it. */
    .qa-fab__ico {
        display: grid;
        place-items: center;
        flex: 0 0 auto;
        width: 2.1rem;
        height: 2.1rem;
        border-radius: 10px;
        background: rgba(244, 123, 32, 0.16);
        color: var(--nuh-orange-soft, #ff9a4d);
    }
    .qa-fab__ico .icon { width: 1.15rem; height: 1.15rem; }

    .qa-fab__txt { flex: 1 1 auto; min-width: 0; }

    .qa-fab__go {
        flex: 0 0 auto;
        width: 1rem;
        height: 1rem;
        opacity: 0.45;
        color: var(--knms-muted, #b7a8e0);
    }

    @keyframes qa-fab-rise {
        from { opacity: 0; transform: translateY(10px) scale(0.98); }
        to   { opacity: 1; transform: none; }
    }
    @keyframes qa-fab-fade {
        from { opacity: 0; }
        to   { opacity: 1; }
    }

    /* --- The public theme --------------------------------------------
       The defaults above are app.css's dark palette, which is right on
       the member screens. It is wrong on every page carrying
       body.nuh-page: that theme is a LIGHT one — white cards, deep-blue
       ink, a pale sky behind them — and a violet card rising out of its
       deep-blue bar read as a piece of a different product, which is the
       exact complaint the console block below is answering.

       So the sheet is repainted in the theme's own tokens. They are
       declared on body.nuh-page in unityhub.css, which loads AFTER this
       sheet; nothing there names .qa-fab, so these rules stand
       unopposed, and the var() fallbacks cover the case where that file
       has not loaded at all. */
    body.nuh-page .qa-fab__sheet {
        background: var(--nuh-card, #ffffff);
        border-color: var(--nuh-line, rgba(27, 86, 164, 0.14));
        box-shadow:
            0 1px 0 rgba(255, 255, 255, 0.9) inset,
            0 24px 48px -12px rgba(20, 57, 110, 0.34);
    }
    body.nuh-page .qa-fab__head { color: var(--nuh-ink-muted, #52708c); }
    body.nuh-page .qa-fab__act { color: var(--nuh-ink, #17324f); }
    body.nuh-page .qa-fab__act:hover,
    body.nuh-page .qa-fab__act:focus-visible {
        background: rgba(27, 86, 164, 0.07);
        color: var(--nuh-ink, #17324f);
    }
    /* The AA-safe orange, not the fill orange: this one is sitting on a
       pale tile on white, where #f47b20 is the wrong side of legible. */
    body.nuh-page .qa-fab__ico { color: var(--nuh-orange-ink, #c2530a); }
    body.nuh-page .qa-fab__go { color: var(--nuh-ink-muted, #52708c); }

    /* --- The console ------------------------------------------------
       Below 900px the admin console keeps the site's bottom bar and adds
       its own "Admin menu" pill above it: fixed, 9.6rem wide, sitting
       0.85rem above --footer-h on the LEFT. The circle is centred and
       stops 44px above the bar's foot, so the two clear each other by
       roughly 18px at 360px — measured, not assumed. Two things still
       needed saying:

       a) the pill is z-index 72 and the bar is 50, so while the sheet is
          open the pill would float ON TOP of the scrim. It is taken out
          of the way for as long as the sheet is open, exactly the way
          admin-shell.css already takes it out of the way while its own
          drawer is open.
       b) the console repaints the bar white below 900px, so the sheet is
          repainted with it — a violet card under a white bar reads as a
          piece of somebody else's site.

       :has() carries (a); where it is unsupported the sheet still opens,
       closes and works, with the pill visible over the dimming. */
    body.admin-area .qa-fab__sheet {
        background: #ffffff;
        border-color: var(--knms-border, #e6ecf5);
        box-shadow: 0 24px 48px -12px rgba(16, 34, 68, 0.28);
    }
    body.admin-area .qa-fab__head { color: var(--knms-muted, #667085); }
    body.admin-area .qa-fab__act { color: var(--knms-text, #101828); }
    body.admin-area .qa-fab__act:hover,
    body.admin-area .qa-fab__act:focus-visible {
        background: rgba(16, 34, 68, 0.06);
        color: var(--knms-text, #101828);
    }
    body.admin-area .qa-fab__ico { color: var(--nuh-orange-ink, #c2530a); }
    /* The console is white on white, so the collar changes sides: a
       hairline of the console's own border colour, and the lift comes
       from a cooler, deeper shadow than the public themes want. */
    body.admin-area .qa-fab__btn {
        box-shadow:
            0 0 0 3px rgba(255, 255, 255, 0.95),
            0 0 0 4px rgba(16, 34, 68, 0.1),
            0 4px 10px -2px rgba(16, 34, 68, 0.22),
            0 12px 26px -8px rgba(196, 83, 10, 0.5);
    }
    body.admin-area:has(.qa-fab[open]) .asb-burger,
    body.admin-area:has(.qa-fab[open]) .asb-switch {
        opacity: 0;
        visibility: hidden;
    }

    /* ...and (c), which measurement found and arithmetic did not. The pill
       is parked at `--footer-h + 0.85rem`, which put its foot at 85.6px
       above the viewport bottom; the circle's head is at 100px above it.
       They miss each other vertically by nothing at all — at 360px the
       two boxes share a 15px by 14px corner, right where the pill's own
       badge sits.

       The pill moves, not the circle. "Above the bottom bar" is what that
       offset has always meant, and the bottom bar now has a button
       standing on it; the circle cannot answer instead, because it cannot
       go lower without eating the tab beneath it (decision 2) and cannot
       go sideways without leaving the centre.

       Only where they can actually meet. The circle's left edge is at
       50vw - 28px and the pill's right edge is a fixed 167.2px, so they
       stop touching at about 390px of viewport; 480px is that boundary
       with room for the larger root font the a11y text control can set.

       `html` is on the front of the selector to win a specificity tie:
       admin-shell.css says the same thing at (0,2,1) and, being injected
       from includes/admin-nav.php inside <body>, is the later sheet. One
       extra type selector settles it, which is cheaper than !important
       and leaves the rule overridable in turn. */
    @media (max-width: 480px) {
        html body.admin-area .asb-burger,
        html body.admin-area .asb-switch {
            bottom: calc(var(--footer-h, 72px) + 3.35rem + env(safe-area-inset-bottom, 0px));
        }
    }

    /* Motion policy. The sheet and the scrim still appear and disappear —
       they are the only route to the actions — but they arrive rather
       than travel, and the "+" turns into an "x" without the spring.
       Section 14 of this sheet takes the same line. */
    @media (prefers-reduced-motion: reduce) {
        .qa-fab__btn,
        .qa-fab__glyph,
        .qa-fab__act,
        .qa-fab__sheet,
        .qa-fab__scrim {
            transition: none;
            animation: none;
        }
        .qa-fab__btn:active { transform: translateX(-50%); }
    }
}

/* ===================================================================
   18. Light-theme correction — the member area
   -------------------------------------------------------------------
   members/dashboard.php, billing.php, directory.php and journey.php
   were already calling apply_public_theme(); the other ten member
   screens were not, so a member moving from their dashboard to their
   profile crossed from the light product into the old dark violet one.
   Those ten now call it too, and this section is the other half of that
   change: the components they are built from were drawn for a dark
   ground, and a theme flip alone leaves them wrong on a white card.

   unityhub.css already carries a large body.nuh-page layer for the
   GENERIC components — .card, .btn, .form-group, .badge-pill, tables,
   .dashboard-header. Nothing here duplicates that. What is here is the
   set those pages use that the public pages never did: the family tree
   canvas, the profile identity header and its section accordion, the
   dependants and endorsement rows, the completeness meter, the stat
   tiles, the notification list and the avatar chip.

   Three mechanical notes, because each one is a trap that was walked
   into while writing this:

   1. LOAD ORDER IS app.css -> premium.css -> profile.css -> unityhub.css.
      This sheet is EARLY, so every rule here has to out-rank its target
      on specificity rather than on order. `body.nuh-page .x` (0,2,1)
      beats a bare `.x` from app.css or profile.css, which is why the
      plain form works for most of this section.

   2. AGAINST unityhub.css THAT IS NOT ENOUGH. Its rules are also
      `body.nuh-page .x` and it loads later, so a tie goes to it. The
      one place that bites is `body.nuh-page .card a` (0,2,2), which
      colours EVERY link in a card brand blue — including links that
      are buttons. `<a class="btn">` inside a card came out blue on
      orange at 2.05:1. Those selectors carry the extra class to reach
      (0,3,2) and win honestly. No !important anywhere in this section.

   3. THE TOKENS ARE NOT ALWAYS THERE. apply_public_theme() is a no-op
      when the site is set to the classic landing template, and then
      unityhub.css is not loaded at all — so --nuh-* is undefined. Every
      rule here is scoped to body.nuh-page, which is set by the same
      function that loads the sheet, so the two can never come apart.
      Page markup, which cannot be scoped that way, keeps using the
      shared .cell-sub class instead of naming either theme's token.
   =================================================================== */

/* --- The native widgets ------------------------------------------
   app.css declares `:root { color-scheme: dark }` (its line 1381). That
   is not a colour, it is an instruction to the ENGINE: paint the
   controls the browser draws itself — checkbox ticks, the select popup,
   the date picker, scrollbars — for a dark page. No rule on our side
   reaches inside them. On the light member pages it was handing out
   black-filled checkboxes on white cards and a charcoal dropdown over a
   white form; the tree's search box and its two filter selects rendered
   as white text on #3b3b3b, which no contrast check flags and which
   nobody would mistake for the light product.

   color-scheme inherits, so redeclaring it on the body is the whole
   fix. admin-shell.css made exactly this correction for the console
   (its section 2) when that shell went light; this is the member area's
   copy of it, scoped the same way. It also lands on the four pages that
   were already light — they had the same dark widgets, and this is the
   direction of the fix, not a change of intent. */
body.nuh-page { color-scheme: light; }

/* Tick boxes. unityhub's light field treatment matches
   `body.nuh-page .form-group input` — every input, checkboxes included —
   and gives it a white fill, a blue border and an inset shadow. On a text
   box that is the point; on a checkbox it paints a box AROUND the box.
   Worse, app.css sets `.form-group input { width: 100% }` unconditionally,
   so the privacy and notification tick boxes are already a thousand
   pixels wide — a stretch that pre-dates this change and is there on the
   dark theme too, but which was invisible until the light theme drew a
   white slab and a border on it and stranded the label at the far end of
   the row.

   The paint is dropped and the width is returned to the glyph. The label
   WRAPS the input on every one of these pages, so the thing a member
   clicks is the label, not the input — nothing about what is clickable
   changes here. accent-color makes the tick brand orange rather than the
   engine's default blue, which is only available at all because of the
   color-scheme correction above. */
body.nuh-page .form-group input[type="checkbox"],
body.nuh-page .form-group input[type="radio"] {
    width: auto;
    flex: none;
    padding: 0;
    background: none;
    border: 0;
    box-shadow: none;
    accent-color: var(--nuh-orange-ink);
}

/* --- Links that are buttons --------------------------------------
   See note 2. Restores the button's own ink inside a card; without it
   the primary action on the profile, family and tree pages reads brand
   blue on brand orange. The :hover pair is restated because unityhub's
   .card a:hover is equally specific and would take it back on hover. */
body.nuh-page .card a.btn,
body.nuh-page .card a.btn:hover { color: #fff; }
body.nuh-page .card a.btn.btn-secondary,
body.nuh-page .card a.btn.btn-secondary:hover { color: var(--nuh-blue-deep); }

/* --- The destructive button ---------------------------------------
   Same shape of problem, and a worse consequence. app.css paints
   .btn-danger red (0,1,0); unityhub's `body.nuh-page .btn` (0,2,1)
   out-ranks it, so on a light page "Remove" and "Revoke" came out in
   the same brand orange as "Save" — the destructive action lost the
   one signal that says do not click this by accident. Restated one
   class higher so it wins, in a red that carries white text at AA. */
body.nuh-page .btn.btn-danger {
    background: linear-gradient(180deg, #d0453c 0%, #a3231a 100%);
    box-shadow: 0 6px 16px rgba(163, 35, 26, 0.26);
    color: #fff;
}
body.nuh-page .btn.btn-danger:hover { box-shadow: 0 10px 22px rgba(163, 35, 26, 0.36); }
body.nuh-page .card a.btn.btn-danger,
body.nuh-page .card a.btn.btn-danger:hover { color: #fff; }

/* --- The avatar chip ----------------------------------------------
   app.css builds the initials disc as purple -> gold -> cyan, which is
   the dark theme's accent set. On white it lands pale (white initials
   on a mid grey-blue, 2.58:1) and reads as a placeholder rather than a
   person. Restated in the brand's own two colours, dark enough that
   white initials clear AA, and the purple glow dropped — a glow needs
   a dark ground to be a glow, and on white it is a smudge.
   This chip is also on the directory, which was already light; it was
   failing there too and is fixed by the same rule. */
body.nuh-page .avatar {
    background: linear-gradient(135deg, var(--nuh-blue) 0%, var(--nuh-orange-ink) 100%);
    box-shadow: 0 0 0 2px #fff;
    text-shadow: none;
}

/* --- Family tree (members/tree.php) --------------------------------
   The whole canvas was drawn on the dark surface: violet node cards,
   gold names, purple connectors, a cyan filter count. Nothing about the
   contrast was wrong on violet — which is exactly why a contrast check
   alone does not find this. It is wrong because it is a dark-violet
   diagram sitting in the middle of a white page. */
body.nuh-page .tree-node {
    background: var(--nuh-card);
    border: 1px solid var(--nuh-line);
    box-shadow: var(--nuh-shadow-sm);
}
body.nuh-page .tree-node-name { color: var(--nuh-blue-deep); }
body.nuh-page .tree-node-meta,
body.nuh-page .tree-band-label,
body.nuh-page .tree-control-label,
body.nuh-page .tree-filter-count,
body.nuh-page .tree-legend { color: var(--nuh-ink-muted); }
body.nuh-page .tree-node-photo { border-color: var(--nuh-line); }
body.nuh-page .tree-node-initials {
    background: linear-gradient(135deg, var(--nuh-blue) 0%, var(--nuh-orange-ink) 100%);
    border-color: transparent;
    color: #fff;
}
/* "Centre on them" is a link inside a card — note 2 again. */
body.nuh-page .card a.tree-node-open { color: var(--nuh-orange-ink); }
/* The person the tree is built around. The ring carries the meaning, so
   it stays a ring; only the colour and the glow change. */
body.nuh-page .tree-node.is-root {
    background: linear-gradient(135deg, rgba(27, 86, 164, 0.09), rgba(244, 123, 32, 0.09));
    box-shadow: 0 0 0 2px var(--nuh-blue);
}
body.nuh-page .tree-node.is-root .tree-node-photo { border-color: var(--nuh-blue); }
/* Somebody with no account: dashed frame, and initials in a flat tint
   rather than the brand gradient, so "not a member yet" stays legible
   as a difference. */
body.nuh-page .tree-node.is-external { border-color: rgba(27, 86, 164, 0.35); }
body.nuh-page .tree-node.is-external .tree-node-initials {
    background: rgba(27, 86, 164, 0.12);
    color: var(--nuh-ink-muted);
}
body.nuh-page .tree-node.is-match,
body.nuh-page .tree-node.is-focused {
    border-color: var(--nuh-orange);
    box-shadow: 0 0 0 2px rgba(244, 123, 32, 0.35);
}
/* The connectors are SVG paths app.js draws; only their stroke is set
   in CSS, so this is all it takes to bring them across. */
body.nuh-page .tree-link { stroke: var(--nuh-blue); opacity: 0.5; }
body.nuh-page .tree-link.is-unlinked { stroke: var(--nuh-ink-muted); }
body.nuh-page .tree-link.is-active { stroke: var(--nuh-orange); }
body.nuh-page .tree-legend { border-top-color: var(--nuh-line); }
body.nuh-page .tree-key { border-top-color: var(--nuh-blue); }
body.nuh-page .tree-key-external { border-top-color: var(--nuh-ink-muted); }
/* The tree's own search and filter controls are the only fields on any
   of these pages that are NOT inside a .form-group — they sit in a bare
   <label class="tree-filter-field"> — so unityhub's light field
   treatment never reached them. Same declarations, same look, so the
   filters match every other input a member meets. */
body.nuh-page .tree-filter-field input,
body.nuh-page .tree-filter-field select {
    background: #fff;
    color: var(--nuh-ink);
    border: 1px solid rgba(27, 86, 164, 0.28);
    box-shadow: inset 0 1px 2px rgba(20, 57, 110, 0.06);
    border-radius: var(--radius-sm, 8px);
}
body.nuh-page .tree-filter-field input:focus,
body.nuh-page .tree-filter-field select:focus {
    border-color: var(--nuh-blue);
    box-shadow: 0 0 0 3px rgba(27, 86, 164, 0.18);
    outline: none;
}
body.nuh-page .tree-filter-field input::placeholder { color: #93a8bd; }

/* --- The one icon with a colour of its own -------------------------
   Every glyph in includes/icons.php is drawn in currentColor except
   'check-box', which is a FILLED TILE: a violet #7c3aed square with a
   white tick, chosen to match the dark theme's reference artwork. It is
   the marker beside every capability in the profile's role panel, the
   badge on the endorsements header and one of the "More about you"
   tiles, so on three of these pages a lone violet chip survived
   everything else going light.

   Corrected here rather than in icons.php because that file is shared
   with the dark theme and the console, where the violet is right. A
   presentation attribute like fill="" loses to any CSS rule at all, so
   scoping the repaint to body.nuh-page is enough — the hex in the
   selector names exactly the value being corrected, and if the icon is
   ever recoloured this simply stops matching rather than fighting it. */
body.nuh-page .icon rect[fill="#7c3aed"] {
    fill: var(--nuh-orange-ink);
    stroke: var(--nuh-orange);
}

/* --- The page header's accent rule ---------------------------------
   .dashboard-header::after is a 3px bar of purple -> gold -> cyan with a
   cyan glow, and it runs under the title of six of these ten pages. It
   is the dark theme's accent set in its purest form; on a white header
   card it is the one thing that still said "old product" after
   everything else had gone light. */
body.nuh-page .dashboard-header::after {
    background: linear-gradient(90deg, var(--nuh-blue), var(--nuh-orange));
    box-shadow: none;
}

/* --- Stat tiles (profile, family) ----------------------------------
   Dark violet panels with gold numbers. They passed every contrast test
   and still looked like a different site. The lit top edge is kept —
   it is what makes a tile a tile — repainted in the brand pair. */
body.nuh-page .stat-tile {
    background: var(--nuh-card);
    border: 1px solid var(--nuh-line);
    box-shadow: var(--nuh-shadow-sm);
}
body.nuh-page .stat-tile::before {
    background: linear-gradient(90deg, var(--nuh-blue), var(--nuh-orange));
    box-shadow: none;
    opacity: 0.85;
}
body.nuh-page .stat-tile .label { color: var(--nuh-ink-muted); }
/* The number itself is GRADIENT INK, not a colour: section 12 of this
   sheet clips a gold gradient to the glyphs and sets
   -webkit-text-fill-color: transparent. A plain `color` here would be
   ignored, and a contrast checker skips the element entirely because
   the fill is transparent — so a gold "41%" on a white tile passes
   every number and is still barely readable. Restated the way section
   15 restates the hero headings: same technique, ink that belongs to
   the light ground, and the gold drop-shadow retired because a glow
   needs something to glow against. Same specificity as section 12's
   rule (0,3,1) and later in the file, so it wins without escalation. */
/* The fallback first: no clip support, or the premium layer switched
   off at Site Settings, and the number is solid brand ink. */
body.nuh-page .stat-tile .value { color: var(--nuh-blue-deep); text-shadow: none; }
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
    body.nuh-page .stat-tile .value {
        background-image: linear-gradient(180deg,
            var(--nuh-blue, #1b56a4) 0%,
            var(--nuh-blue-deep, #14396e) 62%,
            var(--nuh-orange-ink, #c2530a) 130%);
        -webkit-background-clip: text;
                background-clip: text;
        color: transparent;
        -webkit-text-fill-color: transparent;
        filter: none;
    }
}

/* --- Completeness meter (profile) ----------------------------------
   The percentage was --knms-heading, which computes to near-white here:
   white text on a white card, 1.12:1. It was the single least readable
   thing the theme flip exposed. */
body.nuh-page .completeness-value { color: var(--nuh-blue-deep); }
body.nuh-page .completeness-bar { background: rgba(27, 86, 164, 0.12); }
body.nuh-page .completeness-fill {
    background: linear-gradient(90deg, var(--nuh-blue), var(--nuh-orange));
    box-shadow: none;
}
body.nuh-page .completeness-fill.is-good {
    background: linear-gradient(90deg, #1c7a45, var(--nuh-blue));
}
body.nuh-page .gap-list { color: var(--nuh-ink-muted); }

/* --- Dependants, suggestions, endorsement requests (family,
       endorsements) --------------------------------------------------
   All three are bordered rows. Their frames were --knms-border (a deep
   violet) and gold, both of which read as a stray colour on white; the
   name inside a dependant row was --knms-heading, i.e. invisible. */
body.nuh-page .dependent-row,
body.nuh-page .suggestion-row {
    border-color: var(--nuh-line);
    background: rgba(27, 86, 164, 0.03);
}
body.nuh-page .dependent-row { border-color: rgba(244, 123, 32, 0.4); }
body.nuh-page .dependent-name strong { color: var(--nuh-blue-deep); }
body.nuh-page .dependent-photo { border-color: var(--nuh-line); }
body.nuh-page .dup-num { color: var(--nuh-orange-ink); }
body.nuh-page .endorsement-request {
    border: 1px solid var(--nuh-line);
    border-left: 3px solid var(--nuh-orange);
}

/* --- Notifications --------------------------------------------------
   The unread marker is a gradient border-image (purple, gold, cyan) and
   the type icon sits on the same three. Repainted in the brand pair; the
   icon glyph itself is already #fff and stays that way. */
body.nuh-page .notification-item.is-unread {
    border-image: linear-gradient(180deg, var(--nuh-blue), var(--nuh-orange)) 1;
    box-shadow: var(--nuh-shadow-sm);
}
body.nuh-page .notification-icon {
    background: linear-gradient(135deg, var(--nuh-blue) 0%, var(--nuh-orange-ink) 100%);
    box-shadow: none;
}

/* --- Field status (profile, dependant, family forms) ----------------
   The pincode widget's "found it" line is --knms-success (#34d399), a
   mint that needs a dark ground; on white it is 1.92:1. unityhub.css
   states a light-theme green for its own .is-ok variant — this is the
   same green, for the variant the shared widget actually emits. */
body.nuh-page .field-status.is-available { color: #1c7a45; font-weight: 600; }
body.nuh-page .field-status.is-taken { color: #a3231a; }
body.nuh-page .field-status.is-checking { color: var(--nuh-ink-muted); }

/* --- The file picker (profile photo) --------------------------------
   app.css draws ::file-selector-button as a gold pill with a gold glow —
   the dark theme's accent, and the only control on the profile page that
   was still wearing it. The glow goes with it: on white it is a haze
   round the button rather than light coming off it. */
body.nuh-page .form-group input[type="file"]::file-selector-button,
body.nuh-page .form-group input[type="file"]::-webkit-file-upload-button {
    background: linear-gradient(180deg, var(--nuh-orange) 0%, var(--nuh-orange-ink) 100%);
    color: #fff;
    box-shadow: 0 4px 12px rgba(194, 83, 10, 0.24);
}
body.nuh-page .form-group input[type="file"]::file-selector-button:hover,
body.nuh-page .form-group input[type="file"]::-webkit-file-upload-button:hover {
    box-shadow: 0 6px 16px rgba(194, 83, 10, 0.34);
}

/* --- Photo placeholder (profile) ------------------------------------ */
body.nuh-page .photo-preview { border-color: var(--nuh-line); }
body.nuh-page .photo-preview-empty {
    background: linear-gradient(135deg, var(--nuh-blue) 0%, var(--nuh-orange-ink) 100%);
    color: #fff;
}

/* ===================================================================
   18a. Light-theme correction — assets/css/profile.css
   -------------------------------------------------------------------
   The profile identity header and its section accordion live in their
   own sheet, loaded on that page only. Every surface in it is the dark
   theme's: violet glass, gold accents, near-white type. It loads AFTER
   this file, so these all rely on note 1 — body.nuh-page beats its bare
   class selectors on specificity. The alternative was editing
   profile.css itself, which another change is holding open.
   =================================================================== */

/* The identity line under the name: --knms-muted is a pale violet that
   came out at 1.83:1 on the header's tinted card. */
body.nuh-page .identity__number,
body.nuh-page .identity__work,
body.nuh-page .identity__where { color: var(--nuh-ink-muted); }
body.nuh-page .identity__figure .avatar {
    box-shadow: 0 0 0 2px #fff, 0 0 0 4px rgba(27, 86, 164, 0.22);
}

/* The role tag. Three tiers, and two of them were unreadable on white:
   "Member" is near-white type on a 6%-white fill, and the staff tier is
   cyan on translucent cyan. The elected-office tier is deliberately
   LEFT ALONE — it is gold on purpose, it matches .office-badge in the
   already-light directory, and dark ink on gold reads fine here. */
body.nuh-page .role-tag--member {
    color: var(--nuh-blue-deep);
    background: rgba(27, 86, 164, 0.1);
    border-color: rgba(27, 86, 164, 0.22);
}
body.nuh-page .role-tag--member:hover {
    border-color: var(--nuh-blue);
    box-shadow: 0 4px 12px rgba(27, 86, 164, 0.18);
}
body.nuh-page .role-tag--staff {
    color: var(--nuh-blue-deep);
    background: rgba(27, 86, 164, 0.14);
    border-color: rgba(27, 86, 164, 0.3);
}
body.nuh-page .role-tag--staff:hover {
    border-color: var(--nuh-blue);
    box-shadow: 0 4px 12px rgba(27, 86, 164, 0.18);
}
body.nuh-page .role-tag:focus-visible { outline-color: var(--nuh-blue); }

/* What the tag means, in the panel it opens. This is the site's glass
   formula — translucent violet plus a backdrop blur — which on a white
   page is a violet slab. Restated as the light theme's own tinted panel,
   and the blur dropped: there is nothing behind it worth blurring. */
body.nuh-page .role-detail {
    background: rgba(27, 86, 164, 0.05);
    border: 1px solid var(--nuh-line);
    border-left: 3px solid var(--nuh-orange);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}
body.nuh-page .role-detail__blurb,
body.nuh-page .role-detail__caps li { color: var(--nuh-ink); }
body.nuh-page .role-detail__caps-label { color: var(--nuh-ink-muted); }
body.nuh-page .role-detail__caps .icon { color: var(--nuh-orange-ink); }

/* "More about you". These are dark violet cards (--knms-bg-elev) with
   near-white type; they were the most obviously wrong thing on the page
   after the flip — three dark tiles at the bottom of a white one. The
   .card a form is note 2: unityhub colours every card link brand blue,
   which the strong and the sub-line inside inherit. */
body.nuh-page .card a.profile-link,
body.nuh-page .card a.profile-link:hover {
    background: var(--nuh-card);
    border-color: var(--nuh-line);
    color: var(--nuh-ink);
}
body.nuh-page .card a.profile-link strong { color: var(--nuh-blue-deep); }
body.nuh-page .card a.profile-link:hover {
    border-color: var(--nuh-orange);
    box-shadow: 0 6px 16px rgba(27, 86, 164, 0.12);
}
body.nuh-page .profile-link .icon { color: var(--nuh-orange-ink); }
body.nuh-page .profile-link:focus-visible { outline-color: var(--nuh-blue); }

/* The section accordion: count, meter and chevron. --knms-muted again
   for the count (2.17:1), a violet meter track, a purple-to-cyan fill,
   and a mint "done" state that is 1.92:1 on white. */
body.nuh-page .psec__count,
body.nuh-page .psec__chev { color: var(--nuh-ink-muted); }
body.nuh-page .psec--done .psec__count { color: #1c7a45; }
body.nuh-page .psec__meter { background: rgba(27, 86, 164, 0.12); }
body.nuh-page .psec__fill { background: linear-gradient(90deg, var(--nuh-blue), var(--nuh-orange)); }
body.nuh-page .psec--done .psec__fill { background: linear-gradient(90deg, #1c7a45, var(--nuh-blue)); }
body.nuh-page .psec__foot { border-top-color: var(--nuh-line); }
body.nuh-page .psec__summary:focus-visible { outline-color: var(--nuh-blue); }
body.nuh-page .psec-note {
    background: rgba(27, 86, 164, 0.05);
    border: 1px solid var(--nuh-line);
    border-left: 3px solid var(--nuh-orange);
    color: var(--nuh-ink-muted);
}
body.nuh-page .psec-note .icon { color: var(--nuh-orange-ink); }

/* "Worth adding next". unityhub already gives the link its blue; what it
   does not touch is the violet underline and the gold hover. */
body.nuh-page .gap-link { text-decoration-color: var(--nuh-line); }
body.nuh-page .card a.gap-link:hover,
body.nuh-page .card a.gap-link:focus-visible {
    color: var(--nuh-orange-ink);
    text-decoration-color: var(--nuh-orange);
}
body.nuh-page .gap-link:focus-visible { outline-color: var(--nuh-blue); }
/* The brief mark on the field a suggestion scrolled to. Gold-on-gold-glow
   is invisible on white, and a mark nobody sees is the same as no mark. */
body.nuh-page .is-gap-target {
    outline-color: var(--nuh-orange);
    box-shadow: 0 0 0 4px rgba(244, 123, 32, 0.18);
}
