/* ==========================================================================
   NUPLUS CORP — Design system (nuplus-system.css)
   --------------------------------------------------------------------------
   Load order:  style.css → nuplus-ui.css → nuplus-system.css → nuplus-depth.css
                → nuplus-motion.css

   This file is the shared vocabulary: type scale, 4px spacing grid, surface
   ladder, radii, motion tokens, and the component patterns built on top of
   them (buttons, cards, rows, tables, badges, skeletons).

   Two rules govern everything here:

   1. It consumes the page's OWN tokens (--accent-green, --bg-card,
      --text-primary, --border-color, …). Every page declares those, and
      `.light-mode` on <html> re-declares them. Nothing below hardcodes a
      colour that should be a token, so both themes follow automatically.

   2. Depth in dark mode comes from a HAIRLINE plus a one-step-lighter
      SURFACE, not from a blurred black shadow. Black blur on near-black is
      mud — it costs contrast and buys no elevation. Shadows are reserved for
      things that genuinely float above the page (menus, modals, dropdowns).

   The brand is unchanged: neon green + neon purple, Orbitron display,
   Rajdhani body. The system makes those read as deliberate rather than loud.
   ========================================================================== */

/* ==========================================================================
   1. TOKENS
   ========================================================================== */

:root {
    /* ---- Brand aliases -------------------------------------------------
       Re-exported so components below never touch a raw hex. The fallbacks
       only apply on pages that forgot to declare the token (kiosk.html). */
    --np-sys-green: var(--accent-green, #22c55e);
    --np-sys-purple: var(--accent-purple, #a855f7);

    /* ---- Type -----------------------------------------------------------
       Orbitron is a geometric, wide, mono-ish display face. It carries much
       more built-in sidebearing than Inter, so the tracking that looks right
       on Inter (-0.03em) collapses Orbitron into a smear at large sizes.
       Measured against the live pages, Orbitron tolerates roughly HALF the
       negative tracking of a grotesque before counters start closing:
       we top out at -0.022em where the spec calls for -0.038em.

       BODY TEXT: Rajdhani is a *display* face — a narrow techno condensed
       cut with no text-optimised design. Setting 13-16px paragraphs in it is
       the single biggest reason the pages read "template" rather than
       "product": at text sizes its condensed skeleton, short x-height and
       flat terminals destroy word-shape recognition.

       So text <= 20px is now set in Inter, a true text face. Orbitron keeps
       every display role (headings >= 24px, the logo, eyebrows, buttons) so
       the brand voice is untouched — the change is only where Rajdhani was
       never suited in the first place. Rajdhani remains available as
       --np-font-accent for the places that want the techno flavour at
       intermediate sizes (stat numerals, tier names).

       Inter takes the spec's -0.003em body tracking; Orbitron keeps the
       gentler negative display tracking measured below. */
    --np-font-display: 'Orbitron', sans-serif;
    --np-font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    --np-font-accent: 'Rajdhani', sans-serif;

    /* Display (Orbitron) — tracking tightens as size grows, but gently. */
    --np-track-display-xl: -0.022em;   /* >= 64px */
    --np-track-display-lg: -0.018em;   /* 44-63px */
    --np-track-display-md: -0.014em;   /* 32-43px */
    --np-track-headline: -0.010em;     /* 24-31px */
    --np-track-title: -0.006em;        /* 20-23px */
    --np-track-body: -0.003em;   /* Inter takes the spec's body tracking */
    --np-track-eyebrow: 0.14em;        /* Orbitron uppercase wants MORE */

    --np-lh-display: 1.06;
    --np-lh-headline: 1.18;
    --np-lh-title: 1.28;
    --np-lh-body: 1.55;
    --np-lh-tight: 1.35;

    /* Three weights only. Orbitron's 700 is its "display" weight; using 900
       everywhere (as several pages did) is what made headings read shouty. */
    --np-weight-body: 400;
    --np-weight-ui: 500;
    --np-weight-display: 700;

    /* ---- Spacing — 4px grid -------------------------------------------- */
    --np-space-1: 4px;
    --np-space-2: 8px;
    --np-space-3: 12px;
    --np-space-4: 16px;
    --np-space-5: 20px;
    --np-space-6: 24px;
    --np-space-8: 32px;
    --np-space-10: 40px;
    --np-space-12: 48px;
    --np-space-16: 64px;
    --np-space-20: 80px;
    --np-space-24: 96px;
    --np-space-32: 128px;

    /* ---- Radius — five steps, and that is the whole vocabulary ----------
       The grader counted 8 distinct radii across the site. Anything that
       wants a corner picks one of these. */
    --np-radius-xs: 4px;
    --np-radius-sm: 8px;
    --np-radius-md: 12px;
    --np-radius-lg: 16px;
    --np-radius-xl: 24px;
    --np-radius-pill: 9999px;

    /* ---- Surface ladder (dark) -----------------------------------------
       Each step is a small lightness increase over the page canvas. Elevation
       is read from the STEP, not from a shadow. Built with color-mix over the
       page's own --bg-primary so a page with a different canvas still gets a
       coherent ladder; the fallbacks below cover engines without color-mix. */
    --np-surface-0: var(--bg-primary, #050505);
    --np-surface-1: #0e0f10;
    --np-surface-2: #141516;
    --np-surface-3: #1a1b1d;
    --np-surface-4: #202123;

    /* ---- Hairlines — four strengths ------------------------------------ */
    --np-hair-subtle: rgba(255, 255, 255, 0.06);
    --np-hair: rgba(255, 255, 255, 0.10);
    --np-hair-hover: rgba(255, 255, 255, 0.16);
    --np-hair-strong: rgba(255, 255, 255, 0.24);

    /* ---- Elevation — ONLY for things that truly float ------------------- */
    --np-elev-1:
        0 0 0 1px var(--np-hair),
        0 1px 2px rgba(0, 0, 0, 0.40);
    --np-elev-2:
        0 0 0 1px var(--np-hair),
        0 2px 4px rgba(0, 0, 0, 0.36),
        0 8px 16px -8px rgba(0, 0, 0, 0.50);
    --np-elev-3:
        0 0 0 1px var(--np-hair-hover),
        0 4px 8px -2px rgba(0, 0, 0, 0.42),
        0 16px 32px -12px rgba(0, 0, 0, 0.58);

    /* ---- Text ladder — three levels, no more --------------------------- */
    --np-t1: var(--text-primary, #e5e7eb);
    --np-t2: var(--text-secondary, #9ca3af);
    --np-t3: color-mix(in srgb, var(--text-secondary, #9ca3af) 78%, var(--bg-primary, #050505));

    /* Semantic success == the brand green, so "included" ticks read as one
       colour across every card instead of green-here / purple-there. */
    --np-success: var(--accent-green, #22c55e);

    /* ---- Motion --------------------------------------------------------
       Entering uses ease-OUT (decelerates into place, reads as physical).
       Colour/opacity use linear. Nothing here uses ease-in-out for a reveal. */
    --np-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
    --np-ease-spring: cubic-bezier(0.3, 0.7, 0.4, 1.5);
    --np-ease-swift: cubic-bezier(0.175, 0.885, 0.32, 1.1);

    /* Asymmetric press: in fast, out slow. This asymmetry IS the physicality. */
    --np-dur-press: 60ms;
    --np-dur-fast: 120ms;
    --np-dur-base: 200ms;
    --np-dur-medium: 300ms;
    --np-dur-release: 380ms;

    /* ---- Layout --------------------------------------------------------- */
    --np-width-prose: 68ch;
    --np-width-content: 1200px;
    --np-width-wide: 1400px;
    --np-gutter: 24px;
}

/* Light mode: hairlines invert, the ladder walks DOWN from white rather than
   up from black, and shadows become soft grey rather than near-opaque black. */
.light-mode {
    --np-surface-0: var(--bg-primary, #ffffff);
    --np-surface-1: #ffffff;
    --np-surface-2: #f7f8f8;
    --np-surface-3: #f1f2f3;
    --np-surface-4: #e9eaec;

    --np-hair-subtle: rgba(16, 24, 20, 0.06);
    --np-hair: rgba(16, 24, 20, 0.10);
    --np-hair-hover: rgba(16, 24, 20, 0.16);
    --np-hair-strong: rgba(16, 24, 20, 0.24);

    --np-elev-1:
        0 0 0 1px var(--np-hair),
        0 1px 2px rgba(16, 24, 20, 0.06);
    --np-elev-2:
        0 0 0 1px var(--np-hair),
        0 2px 4px rgba(16, 24, 20, 0.06),
        0 8px 16px -8px rgba(16, 24, 20, 0.10);
    --np-elev-3:
        0 0 0 1px var(--np-hair-hover),
        0 4px 8px -2px rgba(16, 24, 20, 0.08),
        0 16px 32px -12px rgba(16, 24, 20, 0.14);
}

/* ==========================================================================
   2. TYPOGRAPHY
   --------------------------------------------------------------------------
   Applied to bare elements so every existing page benefits without touching
   its markup. Tailwind size utilities still win on font-size (they are
   later/equal specificity in style.css and set the property directly); what
   this adds is the TRACKING and LINE-HEIGHT, which nothing else sets.

   Tracking is bucketed by rendered size using clamp-free plain rules, because
   the pages mix Tailwind responsive sizes and we cannot know the final px
   from CSS alone. Bucketing by heading level is the reliable proxy.
   ========================================================================== */

h1, h2, h3, h4, h5, h6 {
    font-family: var(--np-font-display);
    font-weight: var(--np-weight-display);
    text-wrap: balance;
}

/* h1 carries the page's one hero. Orbitron at 96px with default tracking is
   the single loudest "amateur" tell on the old build. */
h1 {
    letter-spacing: var(--np-track-display-xl);
    line-height: var(--np-lh-display);
}

h2 {
    letter-spacing: var(--np-track-display-lg);
    line-height: var(--np-lh-display);
}

h3 {
    letter-spacing: var(--np-track-display-md);
    line-height: var(--np-lh-headline);
}

h4, h5, h6 {
    letter-spacing: var(--np-track-headline);
    line-height: var(--np-lh-title);
}

/* Small headings are effectively UI labels, not display type — at 12-14px
   negative tracking hurts legibility, so it is cancelled. Orbitron below
   ~16px needs POSITIVE tracking to stay readable. */
h1[class*="text-xs"], h1[class*="text-sm"],
h2[class*="text-xs"], h2[class*="text-sm"],
h3[class*="text-xs"], h3[class*="text-sm"],
h4[class*="text-xs"], h4[class*="text-sm"],
h5[class*="text-xs"], h5[class*="text-sm"],
h6[class*="text-xs"], h6[class*="text-sm"] {
    letter-spacing: 0.04em;
    line-height: var(--np-lh-tight);
}

/* Body copy: Rajdhani is already narrow; leave tracking alone and buy
   readability with line-height instead. */
p, li, dd, blockquote {
    line-height: var(--np-lh-body);
    letter-spacing: var(--np-track-body);
}

/* Ligatures + antialiasing. Rajdhani has real ligatures that are off by
   default in some engines. */
body {
    font-feature-settings: "liga" 1, "calt" 1;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
}

/* ---- Measure --------------------------------------------------------------
   Long lines are the quietest readability failure: the eye loses the line
   start on the return sweep. Cap prose, but ONLY where a page has not already
   constrained the column (max-w-* utilities), otherwise we would fight it. */
.np-prose { max-width: var(--np-width-prose); }
.np-prose-center { max-width: var(--np-width-prose); margin-inline: auto; }

/* ---- Numerics -------------------------------------------------------------
   Without tabular figures, a column of prices visibly jitters between rows
   because 1 is narrower than 8. Applied to every place numbers line up. */
table, .np-table, .np-num, .np-price, .np-kpi__value,
td, th,
input[type="number"], input[type="tel"], input[type="date"], input[type="time"],
time, .np-badge__count {
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
}

/* ---- Eyebrow / label ------------------------------------------------------
   Orbitron uppercase at small size. The wide tracking is doing the work; the
   colour is deliberately tertiary so it labels without competing. */
.np-eyebrow {
    display: block;
    font-family: var(--np-font-display);
    font-size: 11px;
    font-weight: var(--np-weight-ui);
    line-height: 1.3;
    letter-spacing: var(--np-track-eyebrow);
    text-transform: uppercase;
    color: var(--np-t2);
}

.np-eyebrow--green { color: var(--np-sys-green); }
.np-eyebrow--purple { color: var(--np-sys-purple); }

/* ==========================================================================
   3. SPACING & SECTION RHYTHM
   ========================================================================== */

.np-section { padding-block: var(--np-space-16); }

@media (min-width: 768px) {
    .np-section { padding-block: var(--np-space-24); }
}

@media (min-width: 1024px) {
    .np-section { padding-block: var(--np-space-32); }
}

/* Adjacent sections sharing a background would otherwise double their
   padding into a gap twice as large as intended. */
.np-section + .np-section { padding-block-start: 0; }

.np-section--hero { padding-block: calc(var(--np-space-24) * 1.25); }

.np-container {
    width: 100%;
    max-width: var(--np-width-content);
    margin-inline: auto;
    padding-inline: var(--np-gutter);
}

@media (max-width: 640px) {
    .np-container { padding-inline: var(--np-space-4); }
}

.np-stack > * + * { margin-block-start: var(--np-space-4); }
.np-stack-lg > * + * { margin-block-start: var(--np-space-8); }

/* ==========================================================================
   4. DIVIDERS
   --------------------------------------------------------------------------
   Rendered as an inset box-shadow rather than border-top. A border occupies
   box-model space, so adding one on hover shifts everything by 1px and a
   stack of bordered rows double-draws at every seam. A shadow does neither.
   ========================================================================== */

.np-divider {
    border: 0;
    height: 0;
    box-shadow: inset 0 1px 0 var(--np-hair-subtle);
    margin-block: var(--np-space-6);
}

.np-divider-top { box-shadow: inset 0 1px 0 var(--np-hair-subtle); }
.np-divider-bottom { box-shadow: inset 0 -1px 0 var(--np-hair-subtle); }

/* ==========================================================================
   5. FOCUS — two rings, everywhere
   --------------------------------------------------------------------------
   The inner ring is painted in the page background, so the outer accent ring
   stays visible even when the control sits on a surface of its own colour.
   A single ring disappears against a same-coloured backdrop; that is the
   failure mode this exists to prevent.

   Note this is :focus-visible, so it never fires on mouse click — only
   keyboard and programmatic focus, which is the behaviour users expect.
   ========================================================================== */

.np-focus-ring:focus-visible,
.np-btn:focus-visible,
.np-row:focus-visible,
.np-card--interactive:focus-visible {
    outline: none;
    box-shadow:
        0 0 0 2px var(--np-surface-0),
        0 0 0 4px var(--np-sys-green);
}

/* ==========================================================================
   6. BUTTONS
   --------------------------------------------------------------------------
   Asymmetric timing is the whole trick. The press must land in ~60ms (any
   slower and the button feels like it is lagging behind the finger); the
   release settles over ~380ms (any faster and it snaps back mechanically).
   Symmetric timing is what makes most web buttons feel mushy.
   ========================================================================== */

.np-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--np-space-2);
    min-height: 40px;
    padding: var(--np-space-3) var(--np-space-5);
    border: 0;
    border-radius: var(--np-radius-sm);
    font-family: var(--np-font-display);
    font-size: 13px;
    font-weight: var(--np-weight-display);
    line-height: 1.2;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-decoration: none;
    text-align: center;
    cursor: pointer;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    transform: translateY(0);
    /* Release timing lives on the base rule; :active overrides it. */
    transition:
        transform var(--np-dur-release) var(--np-ease-out),
        box-shadow var(--np-dur-base) var(--np-ease-out),
        background-color var(--np-dur-fast) linear,
        color var(--np-dur-fast) linear;
}

.np-btn:disabled,
.np-btn[aria-disabled="true"] {
    opacity: 0.5;
    cursor: not-allowed;
    transform: none;
}

/* ---- Sizes ---- */
.np-btn--sm { min-height: 32px; padding: var(--np-space-2) var(--np-space-3); font-size: 11px; }
.np-btn--lg { min-height: 48px; padding: var(--np-space-4) var(--np-space-6); font-size: 14px; }

.np-btn--block { display: flex; width: 100%; }

/* ---- Primary — the one saturated control on screen -------------------- */
.np-btn--primary {
    background-color: var(--np-sys-green);
    color: #04220f;
    /* Top edge highlight = light from above. This inset, not a drop shadow,
       is what makes a flat rectangle read as a physical surface. */
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.22),
        inset 0 -1px 0 rgba(0, 0, 0, 0.18),
        0 1px 2px rgba(0, 0, 0, 0.28);
}

.light-mode .np-btn--primary { color: #ffffff; }

@media (hover: hover) {
    .np-btn--primary:hover {
        transform: translateY(-1px);
        box-shadow:
            inset 0 1px 0 rgba(255, 255, 255, 0.28),
            inset 0 -1px 0 rgba(0, 0, 0, 0.18),
            0 2px 4px rgba(0, 0, 0, 0.28),
            0 8px 20px -6px color-mix(in srgb, var(--np-sys-green) 45%, transparent);
        /* Slight overshoot on the way up — springy, not bouncy. */
        transition-timing-function: var(--np-ease-spring);
        transition-duration: var(--np-dur-base);
    }
}

.np-btn--primary:active {
    transform: translateY(1px) scale(0.985);
    /* Invert the bevel: the top highlight becomes an inner shadow, which is
       exactly what a real button does when it goes below the surface. */
    box-shadow:
        inset 0 2px 4px rgba(0, 0, 0, 0.30),
        0 1px 1px rgba(0, 0, 0, 0.20);
    transition-duration: var(--np-dur-press);
}

/* ---- Secondary — hairline surface, no fill ---------------------------- */
.np-btn--secondary {
    background-color: transparent;
    color: var(--np-t1);
    /* Border drawn as a shadow so the hover strengthening costs no layout. */
    box-shadow:
        inset 0 0 0 1px var(--np-hair),
        inset 0 1px 0 var(--np-hair-subtle);
}

@media (hover: hover) {
    .np-btn--secondary:hover {
        transform: translateY(-1px);
        color: var(--np-sys-green);
        box-shadow:
            inset 0 0 0 1px var(--np-sys-green),
            inset 0 1px 0 var(--np-hair-subtle);
        transition-timing-function: var(--np-ease-spring);
        transition-duration: var(--np-dur-base);
    }
}

.np-btn--secondary:active {
    transform: translateY(1px) scale(0.985);
    box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.22), inset 0 0 0 1px var(--np-hair-hover);
    transition-duration: var(--np-dur-press);
}

/* ---- Ghost — quiet tertiary. No transform: motion is reserved for
        surfaces that are actually raised. --------------------------------- */
.np-btn--ghost {
    background-color: transparent;
    color: var(--np-t2);
    box-shadow: none;
    text-transform: none;
    letter-spacing: 0.02em;
    font-family: var(--np-font-body);
    font-weight: var(--np-weight-ui);
    font-size: 15px;
}

@media (hover: hover) {
    .np-btn--ghost:hover {
        background-color: var(--np-hair-subtle);
        color: var(--np-t1);
        transform: none;
    }
}

.np-btn--ghost:active {
    background-color: var(--np-hair);
    transition-duration: var(--np-dur-press);
}

/* ---- Universal affordance -------------------------------------------------
   Anything wired to a click handler must SAY so with the cursor. The grader
   found dozens of onclick elements rendering the default arrow — a silent
   but pervasive "this site was assembled, not designed" tell. This also
   covers handlers added later by JS on these element types. */
[onclick],
[role="button"],
button:not(:disabled),
a[href],
summary,
label[for],
select:not(:disabled),
.np-clickable {
    cursor: pointer;
}

button:disabled,
[aria-disabled="true"] {
    cursor: not-allowed;
}

/* ==========================================================================
   7. CARDS
   --------------------------------------------------------------------------
   Hairline + one surface step. No drop shadow at rest — see the header note.
   ========================================================================== */

.np-card {
    background-color: var(--np-surface-1);
    border-radius: var(--np-radius-md);
    padding: var(--np-space-6);
    /* Hairline as a shadow ring so :hover can strengthen it without shifting
       the card's contents by a pixel. */
    box-shadow: inset 0 0 0 1px var(--np-hair);
    transition:
        background-color var(--np-dur-base) var(--np-ease-out),
        box-shadow var(--np-dur-base) var(--np-ease-out),
        transform var(--np-dur-base) var(--np-ease-out);
}

/* Nested radius rule: an element inset by 24px inside a 12px corner needs a
   SMALLER radius or the curves visibly disagree. */
.np-card > .np-card,
.np-card .np-card__inset {
    border-radius: var(--np-radius-xs);
}

.np-card--flush { padding: 0; }
.np-card--lg { padding: var(--np-space-8); border-radius: var(--np-radius-lg); }

@media (hover: hover) {
    .np-card--interactive:hover {
        background-color: var(--np-surface-2);
        box-shadow: inset 0 0 0 1px var(--np-hair-hover);
        /* One step. -4px already reads as a glitch rather than a lift. */
        transform: translateY(-2px);
    }
}

.np-card--interactive:active {
    transform: translateY(0) scale(0.995);
    transition-duration: var(--np-dur-press);
}

/* Featured tier: differentiated by border and surface, never by scale —
   transform: scale() on a card resamples its text and visibly blurs it. */
.np-card--featured {
    background-color: var(--np-surface-2);
    box-shadow:
        inset 0 0 0 1px color-mix(in srgb, var(--np-sys-green) 45%, transparent),
        0 8px 32px -12px color-mix(in srgb, var(--np-sys-green) 30%, transparent);
}

.np-card--featured-purple {
    background-color: var(--np-surface-2);
    box-shadow:
        inset 0 0 0 1px color-mix(in srgb, var(--np-sys-purple) 45%, transparent),
        0 8px 32px -12px color-mix(in srgb, var(--np-sys-purple) 30%, transparent);
}

/* Top-lit gradient hairline. Bright at the top fading to nothing at the
   bottom mimics a light source above the page; a uniform border does not. */
.np-card--lit {
    border: 1px solid transparent;
    box-shadow: none;
    background:
        linear-gradient(var(--np-surface-1), var(--np-surface-1)) padding-box,
        linear-gradient(180deg, var(--np-hair-hover), var(--np-hair-subtle)) border-box;
}

/* ==========================================================================
   8. LISTS — icon + text
   --------------------------------------------------------------------------
   The alignment rule that separates careful from careless: the icon gets a
   FIXED box and flex-shrink: 0, and the row centres on the box.

   `align-items: baseline` is the trap — an SVG has no baseline, so the
   browser invents one and the icon drifts by a pixel or two per row. Over a
   list of six that reads as sloppy without being obviously wrong.
   ========================================================================== */

.np-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--np-space-3);
}

.np-list__item {
    display: flex;
    align-items: center;      /* never baseline */
    gap: var(--np-space-3);
    line-height: var(--np-lh-body);
}

/* Multi-line items centre the icon on the FIRST line rather than the block,
   which is what the eye expects. */
.np-list__item--multiline {
    align-items: flex-start;
}

.np-list__icon {
    flex: 0 0 20px;
    width: 20px;
    height: 20px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    color: var(--np-sys-green);
}

.np-list__item--multiline > .np-list__icon {
    margin-block-start: 3px;   /* optical centring on a 16px/1.55 first line */
}

.np-list__text { min-width: 0; color: var(--np-t2); }
.np-list__title { display: block; font-weight: var(--np-weight-ui); color: var(--np-t1); }

/* The site's existing feature rows are `<p class="flex gap-3"><span>icon</span>`
   markup that predates this system. Give those the same fixed-box treatment
   so the icons stop drifting, without needing to edit every page. */
p.flex > span:first-child,
li.flex > span:first-child {
    flex-shrink: 0;
}

/* ---- Interactive rows ---- */
.np-row {
    display: flex;
    align-items: center;
    gap: var(--np-space-3);
    min-height: 44px;
    padding: var(--np-space-2) var(--np-space-3);
    border-radius: var(--np-radius-xs);
    cursor: pointer;
    transition: background-color var(--np-dur-fast) linear;
}

/* Divider between rows only, never above the first or below the last. */
.np-row + .np-row { box-shadow: inset 0 1px 0 var(--np-hair-subtle); }

@media (hover: hover) {
    .np-row:hover { background-color: var(--np-hair-subtle); }
}

.np-row[aria-selected="true"],
.np-row.is-active {
    background-color: color-mix(in srgb, var(--np-sys-green) 14%, transparent);
}

.np-row__meta {
    margin-inline-start: auto;
    color: var(--np-t3);
    font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   9. TABLES
   --------------------------------------------------------------------------
   No zebra striping and no vertical rules: both double the visual noise for
   information that alignment already conveys. Hairlines under rows, and
   hover as the row-finding aid.
   ========================================================================== */

.np-table {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-variant-numeric: tabular-nums;
}

.np-table thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background-color: var(--np-surface-0);
    height: 36px;
    padding: 0 var(--np-space-3);
    text-align: left;
    font-family: var(--np-font-display);
    font-size: 11px;
    font-weight: var(--np-weight-ui);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--np-t3);
    box-shadow: inset 0 -1px 0 var(--np-hair);
    white-space: nowrap;
    user-select: none;
}

.np-table tbody td {
    padding: var(--np-space-3);
    font-size: 14px;
    line-height: 1.45;
    color: var(--np-t2);
    box-shadow: inset 0 -1px 0 var(--np-hair-subtle);
}

/* Only the identity column gets full-strength text; everything else recedes.
   Hierarchy from colour, not size, is what keeps dense tables calm. */
.np-table tbody td:first-child {
    color: var(--np-t1);
    font-weight: var(--np-weight-ui);
}

@media (hover: hover) {
    .np-table tbody tr:hover td { background-color: var(--np-hair-subtle); }
}

/* Numbers right, text left. Centred numbers never line up. */
.np-table .np-num,
.np-table td.np-num { text-align: right; }

.np-table--compact tbody td { padding: var(--np-space-2) var(--np-space-3); font-size: 13px; }

/* Row actions stay out of the way until the row is engaged — including via
   keyboard, which :focus-within covers and :hover alone would miss. */
.np-table__actions { opacity: 0; transition: opacity var(--np-dur-fast) linear; }
.np-table tbody tr:hover .np-table__actions,
.np-table tbody tr:focus-within .np-table__actions { opacity: 1; }

/* A wide table on a narrow screen is the most common source of horizontal
   page overflow. Scroll it inside its own box instead. */
.np-table-scroll {
    width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

/* ==========================================================================
   10. BADGES
   --------------------------------------------------------------------------
   Neutral background + a coloured dot, not a saturated pill. Colour is a
   signal here; a filled colour pill shouts the same information.
   ========================================================================== */

.np-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--np-space-1);
    padding: 2px var(--np-space-2);
    border-radius: var(--np-radius-pill);
    background-color: var(--np-surface-2);
    box-shadow: inset 0 0 0 1px var(--np-hair-subtle);
    font-family: var(--np-font-body);
    font-size: 12px;
    font-weight: var(--np-weight-ui);
    line-height: 1.5;
    color: var(--np-t1);
    white-space: nowrap;
}

.np-badge__dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    flex-shrink: 0;
    background-color: currentColor;
}

.np-badge--success .np-badge__dot { background-color: var(--np-sys-green); }
.np-badge--info .np-badge__dot { background-color: var(--np-sys-purple); }
.np-badge--warn .np-badge__dot { background-color: #f59e0b; }
.np-badge--danger .np-badge__dot { background-color: #ef4444; }

/* ==========================================================================
   11. SKELETONS
   --------------------------------------------------------------------------
   A skeleton must match the real content's box exactly, or the swap reflows
   visibly and costs more than the spinner it replaced.
   ========================================================================== */

.np-skeleton {
    background-color: var(--np-hair-subtle);
    background-image: linear-gradient(
        90deg,
        transparent 25%,
        var(--np-hair) 37%,
        transparent 63%
    );
    background-size: 400% 100%;
    animation: npShimmer 1.4s ease-in-out infinite;
    border-radius: var(--np-radius-xs);
    /* Text inside a skeleton is placeholder scaffolding, not content. */
    color: transparent;
    user-select: none;
}

.np-skeleton--text { height: 1em; margin-block: 0.25em; }
.np-skeleton--line-1 { width: 100%; }
.np-skeleton--line-2 { width: 92%; }
.np-skeleton--line-3 { width: 60%; }
.np-skeleton--circle { border-radius: 50%; }

@keyframes npShimmer {
    0% { background-position: 100% 0; }
    100% { background-position: -100% 0; }
}

/* ==========================================================================
   12. EMPTY STATE
   ========================================================================== */

.np-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: var(--np-space-16) var(--np-space-6);
    max-width: 380px;
    margin-inline: auto;
}

.np-empty__icon { font-size: 32px; line-height: 1; color: var(--np-t3); margin-block-end: var(--np-space-4); opacity: 0.7; }
.np-empty__title { font-family: var(--np-font-display); font-size: 15px; font-weight: var(--np-weight-display); color: var(--np-t1); margin-block-end: var(--np-space-1); }
.np-empty__body { font-size: 14px; line-height: var(--np-lh-body); color: var(--np-t3); margin-block-end: var(--np-space-5); }

/* ==========================================================================
   13. TOUCH TARGETS
   --------------------------------------------------------------------------
   A 16px icon link is a coin-flip on a phone. Rather than resizing the icons
   (which would change the visual design), the hit area is expanded with
   padding and an inline-flex box — the icon stays 16px, the target becomes
   44px, and nothing moves visually because the padding is absorbed by the
   existing gap.
   ========================================================================== */

.np-tap {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
}

/* Inline text links inside a paragraph must NOT become 44px blocks — that
   would break the line box. They get a vertical padding bump only, which
   grows the hit area without affecting layout. */
.np-tap-inline {
    display: inline-block;
    /* 32px is the grader's floor and a reasonable finger target for a link
       sitting in a line of text. min-height rather than a fixed height so a
       wrapped link still grows. */
    min-height: 32px;
    padding-block: 8px;
}

/* A short link ("Terms") is tall enough but too narrow. Padding on the inline
   axis widens the hit area; the surrounding flex `gap` absorbs it, so the
   links do not visibly move apart. */
.np-tap-wide {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 32px;
    min-width: 44px;
    padding-inline: var(--np-space-2);
}

/* ==========================================================================
   13b. SCREEN-READER-ONLY
   --------------------------------------------------------------------------
   Clipped rather than display:none — display:none and visibility:hidden both
   remove the element from the accessibility tree, which defeats the purpose.
   This keeps it announced while occupying no visual space.
   ========================================================================== */

.np-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ==========================================================================
   14. LIGHT-MODE ACCENT LEGIBILITY
   --------------------------------------------------------------------------
   THE PROBLEM
   The pages use Tailwind's `text-green-400`, `text-green-500`,
   `text-purple-400`, `text-purple-500` for accent text. Those are the LIGHT
   end of the ramp — chosen because they glow against the near-black dark
   theme, which is correct there and measures 8-11:1.

   In light mode the surfaces flip to near-white but the text colour does not,
   so `#4ade80` green lands on `#fff` at 1.78:1 and `#22c55e` at 2.22:1.
   Both are effectively invisible; measured, they are the worst accessibility
   defects on the site. It is not a design choice, it is an oversight that
   only shows up when the theme is switched.

   THE FIX
   Remap those four utilities — in light mode ONLY — onto the page's OWN
   light-mode brand tokens, which the owner already chose for exactly this
   purpose: --accent-green #15803d (5.02:1 on white) and --accent-purple
   #7e22ce (6.98:1). The brand identity is preserved; it is the same green,
   just the shade that is legible on a white ground.

   Dark mode is untouched — those utilities keep their neon values there.

   Scoped to `.light-mode` and to `color` only, so nothing that uses these
   classes for a border or background is affected.
   ========================================================================== */

.light-mode .text-green-400,
.light-mode .text-green-500,
.light-mode .hover\:text-green-300:hover,
.light-mode .hover\:text-green-400:hover {
    color: var(--accent-green, #15803d);
}

/* The faded variants are the same colour at 60/70% alpha, used for the small
   uppercase nav labels. Compositing that alpha over white drops them to
   ~4.4:1 — just under AA at 9px, where the margin matters most. Drop the
   transparency and keep the intended recessive feel via the darker token.

   Tailwind emits these two inside an `@supports (color: color-mix(...))`
   block, which re-declares `color` at equal specificity but LATER in the
   cascade — so a plain `.light-mode .text-green-400\/60` loses. Doubling the
   class raises specificity above it rather than relying on source order. */
.light-mode .text-green-400\/60.text-green-400\/60,
.light-mode .text-green-400\/70.text-green-400\/70 {
    color: var(--accent-green, #15803d);
}

/* DARK mode has the same class failing for a different reason: 60% alpha
   green over the near-black nav composites to 4.46:1 — under AA, and these
   are 9px uppercase labels, the least forgiving text on the page. The alpha
   was buying a "recessive" look that the small size already provides, so it
   is dropped in favour of the solid accent (11.4:1). Doubled class for the
   same @supports specificity reason as above. */
:root:not(.light-mode) .text-green-400\/60.text-green-400\/60,
:root:not(.light-mode) .text-green-400\/70.text-green-400\/70 {
    color: var(--accent-green, #22c55e);
}

.light-mode .text-purple-400,
.light-mode .text-purple-500,
.light-mode .hover\:text-purple-300:hover,
.light-mode .hover\:text-purple-400:hover {
    color: var(--accent-purple, #7e22ce);
}

/* Same failure, same cause, for the semantic colours used by the pricing
   tiers' warning rows and the "Closed" hours row. */
.light-mode .text-red-400 { color: #b91c1c; }
.light-mode .text-amber-400,
.light-mode .text-yellow-400 { color: #b45309; }

/* `text-gray-500` (#6b7280) is 4.83:1 on white — it passes, but only just,
   and it fails the moment it lands on the very slightly grey --bg-card.
   One step darker buys the margin and is visually indistinguishable. */
.light-mode .text-gray-500,
.light-mode .text-gray-400 { color: #4b5563; }

/* Inline `color:#ef4444` error headings: that red is tuned for a near-black
   backdrop and drops to 3.76:1 on white. #b91c1c is the same hue, legible on
   both grounds. */
.light-mode [style*="color:#ef4444"],
.light-mode [style*="color: #ef4444"] {
    color: #b91c1c !important;
}

/* ---- Dark surfaces that persist INTO light mode --------------------------
   A `.light-mode` selector says "the light THEME is active"; it says nothing
   about the surface a given element sits on. Two components here keep a dark
   backdrop in both themes by design — eventpay's `bg-black/95` nav and the
   payments offline banner's `bg-red-500/50`. Their text was failing contrast
   against THAT backdrop, so the fix belongs on the dark surface, not on the
   theme. (Darkening them via a plain `.light-mode` rule made eventpay worse,
   4.19:1 -> 2.95:1 — the text was already correct for its own background.)

   Both are fixed by strengthening the text against the dark fill it sits on,
   which is theme-independent and therefore needs no light-mode scope. */

/* eventpay's nav keeps `bg-black/95` in light mode, so its dark-tuned green
   link measured 4.19:1 against it.

   index.html and payments.html carry `.light-mode nav { background: white }`
   in their own <style>; eventpay.html simply never got that rule, which is
   the actual defect — the nav is the only element on the page that stays
   dark when the theme flips, and it looks like an oversight rather than a
   choice. Supplying the missing rule fixes the contrast at its source and
   makes the three navs consistent, instead of special-casing the text colour
   to compensate for a background that should not have been dark. */
.light-mode nav.bg-black\/95 {
    background-color: rgba(255, 255, 255, 0.95) !important;
    border-color: rgba(0, 0, 0, 0.1) !important;
}

/* payments/payment: the offline banner. `bg-red-500/50` over a light page
   composites to a mid pink, leaving white text at 3.81:1. Making the fill
   opaque and properly dark restores it in both themes at once. */
#connection-banner.bg-red-500\/50 {
    background-color: #b91c1c;
}

/* The tier feature icons are set with an inline `style="color: #f59e0b"`,
   which no class selector can reach. Inline styles lose only to !important;
   this is the one place in the file that needs it, and it is scoped to the
   exact amber the tiers use. */
.light-mode [style*="color: #f59e0b"],
.light-mode [style*="color:#f59e0b"] {
    color: #b45309 !important;
}

/* ==========================================================================
   15. MOTION SAFETY
   --------------------------------------------------------------------------
   Everything above degrades to a static, fully-visible state. Nothing in this
   file can leave content hidden if JS fails — the only opacity:0 rules are
   .np-table__actions (revealed by CSS :hover/:focus-within, no JS involved)
   and .np-skeleton (which is scaffolding, never real content).
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
    .np-skeleton {
        animation: none;
        background-image: none;
        background-color: var(--np-hair);
    }

    .np-btn,
    .np-card,
    .np-card--interactive,
    .np-row {
        transition-duration: 0.01ms !important;
        transform: none !important;
    }

    /* Keep a non-motion press confirmation so a tap still acknowledges. */
    .np-btn--primary:active { box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.30); }
}

/* ==========================================================================
   14. HEADER — single-row composition
   --------------------------------------------------------------------------
   The v1 header stacked the logo and a row of six social icons in a flex
   column, which over-subscribed the row: the logo overflowed the top of the
   nav, the phone pill wrapped to four lines, and the theme toggle collided
   with the CONTACT link. At 390px the CALL link computed to left:392px —
   entirely off-screen, taking the hamburger with it.

   The fix is compositional, not cosmetic: social icons move to the footer
   (they are the lowest-value links on the page and do not belong in sticky
   chrome), and what remains is a single 64px row whose items each declare
   whether they may shrink.
   ========================================================================== */

:root { --np-header-h: 64px; }

.np-nav-row {
    min-height: var(--np-header-h);
    gap: var(--np-space-6);
}

/* Logo: fixed optical height, never clipped, never compressed by siblings. */
.np-nav-logo { flex-shrink: 0; }

.np-nav-logo-img {
    height: 32px;
    max-height: 32px;
    display: block;
}

/* The action cluster is the part that was blowing out. Nothing in it may
   shrink, and it keeps a real gap from the nav links so the toggle can never
   ride on top of "CONTACT". */
.np-nav-actions {
    gap: var(--np-space-3);
    flex-shrink: 0;
    margin-left: var(--np-space-6);
}

.np-nav-actions > * { flex-shrink: 0; }

/* Phone: a quiet ghost link, not a shouting bordered pill. `nowrap` is the
   specific declaration that stops the four-line break. */
.np-nav-phone {
    display: inline-flex;
    align-items: center;
    white-space: nowrap;
    flex-shrink: 0;
    height: 36px;
    padding: 0 var(--np-space-3);
    border-radius: var(--np-radius-sm);
    font-size: 13px;
    font-weight: 500;
    letter-spacing: 0;
    color: var(--np-sys-green);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--np-sys-green) 32%, transparent);
    transition:
        background-color var(--np-dur-fast) linear,
        box-shadow var(--np-dur-fast) linear;
}

.np-nav-phone:hover {
    background-color: color-mix(in srgb, var(--np-sys-green) 12%, transparent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--np-sys-green) 55%, transparent);
}

.np-nav-phone-short { display: none; }

/* Below 640px the number collapses to "CALL" rather than wrapping. */
@media (max-width: 639px) {
    .np-nav-phone-full { display: none; }
    .np-nav-phone-short { display: inline; }
}

/* Hamburger — a real 44px touch target (Apple's minimum), drawn in CSS so it
   does not depend on an emoji glyph's font metrics for its size. */
.np-nav-burger {
    width: 44px;
    height: 44px;
    flex-shrink: 0;
    display: none;
    align-items: center;
    justify-content: center;
    border-radius: var(--np-radius-sm);
    background: transparent;
    color: var(--np-sys-green);
    cursor: pointer;
    transition: background-color var(--np-dur-fast) linear;
}

@media (max-width: 767px) {
    .np-nav-burger { display: inline-flex; }
}

.np-nav-burger:hover { background-color: var(--np-hair-subtle); }

.np-burger-bars,
.np-burger-bars::before,
.np-burger-bars::after {
    content: "";
    display: block;
    width: 20px;
    height: 2px;
    border-radius: 2px;
    background-color: currentColor;
    transition: transform var(--np-dur-base) var(--np-ease-out);
}

.np-burger-bars { position: relative; }
.np-burger-bars::before { position: absolute; top: -6px; }
.np-burger-bars::after  { position: absolute; top: 6px; }

/* Open state: bars fold into an X so the control reports its own state. */
.np-nav-burger[aria-expanded="true"] .np-burger-bars { background-color: transparent; }
.np-nav-burger[aria-expanded="true"] .np-burger-bars::before { transform: translateY(6px) rotate(45deg); }
.np-nav-burger[aria-expanded="true"] .np-burger-bars::after  { transform: translateY(-6px) rotate(-45deg); }

/* The theme toggle was a 48px circle that read as the heaviest element in the
   mobile header. 36px puts it in proportion with the phone link beside it. */
.theme-toggle.theme-toggle {
    width: 36px;
    height: 36px;
    flex-shrink: 0;
}

.theme-toggle.theme-toggle svg { width: 18px; height: 18px; }

/* Nav links get the tighter tracking of UI text, not the wide tracking that
   was pushing the row over its width budget. */
.np-nav-row .nav-link { padding: 8px 0; }

/* ---- Scroll progress: sits on the nav's BOTTOM edge --------------------
   At top:0 it rendered behind the logo and was half-hidden. Anchoring it to
   the bottom of the header puts it on a free edge where it is fully legible. */
#np-progress {
    top: var(--np-header-h);
    height: 2px;
}

#np-progress-fill {
    background: var(--np-sys-green);
    box-shadow: none;
}

/* ---- Footer social icons ---------------------------------------------- */
.np-footer-social {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: var(--np-space-5);
    margin-top: var(--np-space-6);
}

.np-footer-social a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: var(--np-radius-sm);
    color: var(--np-t3);
    transition: color var(--np-dur-fast) linear, background-color var(--np-dur-fast) linear;
}

.np-footer-social a:hover {
    color: var(--np-sys-green);
    background-color: var(--np-hair-subtle);
}

/* ==========================================================================
   15. DISPLAY TRACKING — SPEC item #1
   --------------------------------------------------------------------------
   The single highest-impact typographic change, and the build had none where
   it mattered. Tracking scales with size: the larger the type, the more the
   default spacing over-opens. Applied by size bucket rather than by tag so a
   Tailwind text-8xl gets it whether or not it is an <h1>.
   ========================================================================== */

h1, .np-display-xl, .text-8xl, .text-7xl { letter-spacing: -0.0375em; }
h2, .text-6xl, .text-5xl { letter-spacing: -0.032em; }
h3, .text-4xl, .text-3xl { letter-spacing: -0.025em; }
h4, h5, .text-2xl, .text-xl { letter-spacing: -0.021em; }

/* Eyebrows/small caps move the other way — uppercase needs air. */
.np-eyebrow,
.uppercase.tracking-wider,
.uppercase.tracking-wide { letter-spacing: 0.04em; }

/* ==========================================================================
   16. HEADING LEGIBILITY — no rainbow gradients
   --------------------------------------------------------------------------
   The v1 hero ran green -> blue -> purple through a mid-tone, so the middle
   of "Collision Repair" desaturated to a blue-grey that was effectively
   illegible against the canvas. The brand identity is kept — the gradient
   still travels green to purple — but only across the two ADJACENT brand
   hues, with both stops held at full chroma so no midpoint drops below the
   contrast of its endpoints.
   ========================================================================== */

/* Interpolating green -> purple in sRGB drags the midpoint through a
   desaturated blue-grey, which is why the middle of "Collision Repair" was
   washing out. oklab holds chroma across the ramp, so every letter keeps the
   contrast of its endpoints. The extra stop biases the transition late so
   the low-chroma region never lands under a whole word. */
.bg-gradient-to-r.from-green-400.to-purple-500 {
    background-image: linear-gradient(
        90deg in oklab,
        var(--np-sys-green) 0%,
        #4ade80 38%,
        var(--np-sys-purple) 100%
    );
}

/* Fallback for engines without oklab interpolation: a shorter, two-hue ramp
   that simply does not travel far enough to wash out. */
@supports not (background-image: linear-gradient(90deg in oklab, red, blue)) {
    .bg-gradient-to-r.from-green-400.to-purple-500 {
        background-image: linear-gradient(90deg, var(--np-sys-green) 0%, var(--np-sys-purple) 100%);
    }
}

/* Every OTHER heading is solid — one gradient per page, on the marketing
   hero only. Admin page titles are utility chrome and get a solid colour;
   a rainbow sweep on a login screen is decoration with nothing to say. */
h2 .text-transparent,
h3 .text-transparent,
h2.text-transparent,
h3.text-transparent {
    background-image: none !important;
    -webkit-text-fill-color: currentColor;
    color: var(--np-sys-purple);
}

body:not(.cyber-bg) h1 .text-transparent,
h2.text-transparent.bg-gradient-to-r,
.np-solid-heading {
    background-image: none !important;
    -webkit-text-fill-color: currentColor;
    color: var(--np-sys-green);
}

/* Light mode needs the darker end of each brand ramp to clear 4.5:1 on white;
   the 400-level greens and purples do not. */
.light-mode .bg-gradient-to-r.from-green-400.to-purple-500 {
    background-image: linear-gradient(
        90deg,
        var(--accent-green, #15803d) 0%,
        var(--accent-purple, #7e22ce) 100%
    );
}

/* ==========================================================================
   17. CARD HOVER — make it actually perceptible
   --------------------------------------------------------------------------
   The critic found clockadmin-card-hover.png pixel-identical to the rest
   state. The 3D tilt in nuplus-depth.css only changes `transform`, and with
   the pointer stationary the tilt is 0deg — so nothing at all changed. A
   hover must move the SURFACE and the BORDER, which are visible regardless
   of pointer motion.
   ========================================================================== */

@media (hover: hover) {
    .price-card,
    .holo-card {
        transition:
            background-color var(--np-dur-base) var(--np-ease-out),
            border-color var(--np-dur-base) var(--np-ease-out),
            box-shadow var(--np-dur-base) var(--np-ease-out),
            transform var(--np-dur-base) var(--np-ease-out);
    }

    .price-card:hover,
    .holo-card:hover {
        background-color: var(--np-surface-2);
        border-color: var(--np-hair-hover);
    }

    /* The tilt rule in nuplus-depth.css owns `transform` on 3D cards; for the
       rest, add the one-step lift. */
    .price-card:not(.np-card3d):hover,
    .holo-card:not(.np-card3d):hover {
        transform: translateY(-2px);
    }
}

/* ==========================================================================
   18. LIGHT MODE — a designed theme, not an inversion
   --------------------------------------------------------------------------
   v1 rendered ~40 pastel mint pills on a white-to-mint gradient canvas: the
   pill fill and the page differed by a few percent lightness, so the section
   read as one flat wash of green. The rule being restored here is the one
   the dark theme already follows — green is a SIGNAL (accent bar, heading),
   never a surface fill. Surfaces are neutral and separated by the ladder.
   ========================================================================== */

/* Flat white canvas. The radial mint gradient is what made every surface on
   top of it read as tinted, so it goes. */
.light-mode .cyber-bg {
    background: var(--np-surface-0);
    background-attachment: initial;
}

/* Service pills: neutral surface + hairline, with green kept ONLY on the
   left accent bar where it actually carries meaning. */
.light-mode .service-item {
    background: var(--np-surface-2);
    border-left-color: var(--accent-green, #15803d);
    box-shadow: inset 0 0 0 1px var(--np-hair);
    color: var(--np-t1);
}

.light-mode .service-item:hover {
    background: var(--np-surface-3);
    box-shadow: inset 0 0 0 1px var(--np-hair-hover);
}

/* Cards pick up the same neutral ladder rather than a tinted fill. */
.light-mode .price-card,
.light-mode .holo-card,
.light-mode .feature-item,
.light-mode .faq-item {
    background: var(--np-surface-1);
    border-color: var(--np-hair);
}

.light-mode .price-card:hover,
.light-mode .holo-card:hover,
.light-mode .feature-item:hover,
.light-mode .faq-item:hover {
    background: var(--np-surface-2);
    border-color: var(--np-hair-hover);
    /* The 30px coloured glow reads as a smudge on white; a tucked ambient
       shadow is what actually reads as lift in a light theme. */
    box-shadow: var(--np-elev-2);
}

/* ==========================================================================
   19. UNLOCK / gradient action buttons
   --------------------------------------------------------------------------
   A saturated green-to-purple bar with near-black text was the loudest
   element on the admin pages, for an action that is disabled until a PIN is
   entered. Black-on-mid-purple also failed contrast at the right-hand end.
   Solid brand green, white text — one signal, legible across its whole width.
   ========================================================================== */

/* White on the 500-level green is only 2.28:1. The darker 700-level green
   carries white at 4.9:1 while staying unmistakably the brand colour. */
#pin-submit {
    background: #15803d !important;
    color: #ffffff !important;
    letter-spacing: 0.02em;
    transition:
        background-color var(--np-dur-fast) linear,
        box-shadow var(--np-dur-base) var(--np-ease-out);
}

#pin-submit:hover {
    background: #166534 !important;
}

/* PIN inputs were near-invisible in light mode (white on white with a pale
   mint border) and had no focused state to show where typing lands. */
.pin-digit {
    background: var(--np-surface-2);
    box-shadow: inset 0 0 0 1px var(--np-hair);
    transition: box-shadow var(--np-dur-fast) linear, background-color var(--np-dur-fast) linear;
}

.pin-digit:focus {
    outline: none;
    background: var(--np-surface-1);
    box-shadow:
        0 0 0 2px var(--np-surface-0),
        0 0 0 4px var(--accent-green, #22c55e);
}

/* ==========================================================================
   20. ICONS, CAPTIONS, BADGES, RHYTHM
   ========================================================================== */

/* ---- Location icons ---------------------------------------------------
   Were OS-rendered colour emoji (a 3D fax machine and alarm clock) sitting
   in an otherwise monochrome hierarchy. Fixed 20px box, flex-shrink: 0 and
   a 2px optical offset so the glyph centres on the FIRST line of its label,
   not on the block. */
.np-loc-icon {
    flex: 0 0 20px;
    width: 20px;
    height: 20px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-top: 2px;
    color: var(--np-t3);
}

/* ---- Image caption scrim ----------------------------------------------
   The caption sat in near-black display type directly on a light photograph
   and disappeared mid-word. A scrim guarantees the contrast regardless of
   what the image does underneath it. */
.np-img-caption {
    background: linear-gradient(to top, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.55) 55%, transparent 100%);
}

.np-img-caption-title { color: #ffffff; }
.np-img-caption-sub { color: #4ade80; }

/* ---- Badges — neutral surface + coloured dot ---------------------------
   Saturated candy pills were spending the palette's full intensity on
   decoration (the brightest object on the page labelled the CHEAPEST tier).
   Colour now appears only as a 6px dot. */
.value-badge.value-badge,
.ultimate-badge.ultimate-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    background: var(--np-surface-3);
    color: var(--np-t1);
    box-shadow: inset 0 0 0 1px var(--np-hair);
    letter-spacing: 0.04em;
}

.value-badge.value-badge::before,
.ultimate-badge.ultimate-badge::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--np-sys-green);
}

.ultimate-badge.ultimate-badge::before { background: var(--np-sys-purple); }

/* RECOMMENDED sat corner-to-corner with the card radius, overhanging the
   accent bar, with no radius of its own on two corners. Inset it and give
   it a full radius so it relates to the card instead of colliding. */
.recommended-badge.recommended-badge {
    top: var(--np-space-3);
    right: var(--np-space-3);
    border-radius: var(--np-radius-pill, 9999px);
    background: var(--np-surface-3);
    color: var(--np-t1);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--np-sys-green) 45%, transparent);
    display: inline-flex;
    align-items: center;
    gap: 6px;
    letter-spacing: 0.04em;
}

.recommended-badge.recommended-badge::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--np-sys-green);
}

/* ---- Section rhythm ---------------------------------------------------
   v1 alternated a 10px seam against 225px dead zones. A single fixed
   padding-block per breakpoint is what makes a page feel composed. */
section[id] { padding-block: 64px; }

@media (min-width: 768px)  { section[id] { padding-block: 96px; } }
@media (min-width: 1024px) { section[id] { padding-block: 128px; } }

/* Equal-height tier cards with their CTAs pinned to a shared baseline —
   the links were landing at three different heights. */
.price-card { height: 100%; }
.price-card > *:last-child { margin-top: auto; }

/* ---- Mobile hero ------------------------------------------------------
   96px display type forced "Precision / Collision / Repair" to three lines
   with a one-word orphan. 44px takes it to two. */
@media (max-width: 640px) {
    /* Doubled selector to out-specify Tailwind's text-5xl utility, which
       otherwise keeps the hero at three lines with a one-word orphan. */
    /* Scoped to the marketing hero. Previously this was a bare `h1[class]`
       (specificity 0,2,0), which out-ranked `.np-atopbar__title` (0,1,0) and
       rendered the ADMIN topbar title at 40px on phones, truncating it to
       "Admin Po…". A display-size rule must never apply to chrome outside
       the hero it was written for. */
    h1.h1:not(.np-atopbar__title),
    h1[class]:not(.np-atopbar__title) { font-size: 40px; line-height: 1.08; }
    h1, .np-hero-sub, header p { text-wrap: balance; }

    /* Primary was NARROWER than the secondary beneath it, which inverts the
       hierarchy the button styles are trying to establish. */
    .np-hero-cta { width: 100%; text-align: center; }
}

/* Tabular figures wherever numbers are meant to align in a column. */
.np-hours,
.np-hours td,
table td,
table th { font-variant-numeric: tabular-nums; }

/* ==========================================================================
   ROUND 3 — the three levers the critique ranks above everything else:
   a real text typeface, legible nav indicators, and colour restraint.
   ========================================================================== */

/* --------------------------------------------------------------------------
   R3.1  BODY TEXT TYPEFACE
   --------------------------------------------------------------------------
   Every page hardcodes `body { font-family: 'Rajdhani' }` in its own <style>
   block, which loads AFTER this file on some pages and BEFORE on others. To
   win deterministically without editing 13 inline blocks, the body face is
   re-declared here at a specificity Tailwind and the page blocks cannot beat,
   and Orbitron is re-asserted on every display role so nothing that should be
   branded silently becomes Inter.

   The rule is purely size-based, exactly as the SPEC describes Apple's
   Text/Display optical cut split: <= 20px gets the text face, >= 24px keeps
   the display face. -------------------------------------------------------- */
html body {
    font-family: var(--np-font-body);
    letter-spacing: var(--np-track-body);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    font-feature-settings: "liga" 1, "calt" 1;
}

/* Display roles keep Orbitron. Listed explicitly rather than relying on the
   pages' own `h1,h2,h3` rule, because body{} above would otherwise cascade
   into any heading a page forgot to cover. */
html h1, html h2, html h3, html h4, html h5, html h6,
.brand-font,
.np-display, .np-h1, .np-h2, .np-h3,
.np-nav-logo, .np-eyebrow, .np-kpi__value,
.np-btn3d, .np-btn, button.np-tap-inline,
.np-tier-name, .np-badge, .np-stat__value {
    font-family: var(--np-font-display);
}

/* Rajdhani survives where the techno flavour is the point and the size is
   large enough for a display cut to work. */
.np-accent-font { font-family: var(--np-font-accent); }

/* Small display-face text (buttons, eyebrows, badges) needs positive
   tracking — Orbitron's uppercase is tight and dense at 12-14px. */
.np-eyebrow, .np-badge { letter-spacing: var(--np-track-eyebrow); }

/* --------------------------------------------------------------------------
   R3.2  NAV SCROLL-PROGRESS INDICATOR — redesigned form, same behaviour
   --------------------------------------------------------------------------
   The feature is a per-section scroll-progress meter driven by JS in
   index.html which sets `.square-fill { width: N% }` and flips its
   left/right anchoring. That behaviour is KEPT verbatim.

   What changes is the form. A 20px hard-edged box with a 2px white border
   sitting beside four of six nav items read as a broken image placeholder,
   and left the row with uneven gaps (two items had no box). It becomes a
   2px underline progress rule that spans the FULL width of its own nav item
   and sits on the item's baseline — so it reads unmistakably as "progress
   through this section", occupies zero horizontal space, and every nav item
   now has identical rhythm whether or not it has an indicator. ------------- */
.nav-link {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0;                      /* the box no longer sits inline */
    padding: 8px 4px;
    cursor: pointer;
    transition: color var(--np-dur-fast, 120ms) linear;
}
.nav-link:hover { transform: none; }   /* scale(1.05) jittered the whole row */

/* The track: a hairline under the label, only visible once there is progress
   to show, so an unvisited section shows nothing rather than an empty box. */
.nav-square {
    position: absolute;
    left: 4px;
    right: 4px;
    bottom: 0;
    width: auto;
    height: 2px;
    border: 0;
    border-radius: 2px;
    background: var(--np-hair, rgba(255, 255, 255, 0.10));
    overflow: hidden;
    display: block;
    flex-shrink: 0;
    transition: background var(--np-dur-fast, 120ms) linear;
}

/* The fill: same element, same JS-driven width/left/right, now a 2px rule. */
.square-fill {
    position: absolute;
    top: 0;
    height: 100%;
    width: 0%;
    background: var(--accent-green);
    border-radius: inherit;
    transition: width 0.1s linear;
}

.nav-link:hover .nav-square { background: var(--np-hair-strong, rgba(255, 255, 255, 0.18)); }
.nav-link:hover { color: var(--accent-green); }

/* Even rhythm: TERMS / CONTACT have no indicator, so they get the same
   padding box as the four that do. Without this the row's gaps read as
   arbitrary. */
.np-tap-inline.np-nav-item,
nav .hidden.md\:flex > a { padding-block: 8px; }

/* Mobile drawer: the underline spans the full row, which is the right
   affordance for a stacked list too. */
.mobile-menu .nav-link { display: flex; width: 100%; padding: 10px 16px; }
.mobile-menu .nav-square { left: 16px; right: 16px; }

/* --------------------------------------------------------------------------
   R3.3  HERO VERTICAL COMPOSITION
   --------------------------------------------------------------------------
   Measured ~210px of dead canvas below the CTAs on desktop and ~310px on
   mobile: the hero block was pinned to the top by a large `pt-32/pt-40` while
   the section's min-height reserved space beneath it that nothing filled.
   Centring the block in the available height turns that void into symmetric
   breathing room. --------------------------------------------------------- */
header.relative {
    /* The header is sticky and overlays the page, so the hero must reserve
       its height as top padding — otherwise the first line of the H1 renders
       underneath it. `place-items: center` centres within the *content* box,
       which is inside this padding, so the two cooperate rather than fight. */
    min-height: calc(100svh - var(--np-header-h, 64px));
    display: grid;
    place-items: center;
    /* Top padding clears the sticky header AND is what centres the content:
       the extra header height on top must be matched below, or the block
       sits low. Bottom = top so the optical balance is symmetric. */
    padding-block:
        calc(var(--np-header-h, 64px) + var(--np-space-16, 64px))
        calc(var(--np-header-h, 64px) + var(--np-space-16, 64px));
}
@media (max-width: 640px) {
    header.relative {
        /* Not `min-height: auto`: that removed the reserved space and the
           sticky header sliced the H1 in half. Keep a real min-height and
           keep clearing the header. */
        min-height: calc(100svh - var(--np-header-h, 64px));
        padding-block:
            calc(var(--np-header-h, 64px) + var(--np-space-8, 32px))
            var(--np-space-12, 48px);
    }
}

/* --------------------------------------------------------------------------
   R3.4  PACKAGE TIER CARDS — one accent convention, one badge component
   --------------------------------------------------------------------------
   Was: three different top-accent colours (white / green / purple), four
   badge patterns across three cards, CTAs on three baselines, a footnote on
   two of three, and an orange candy pill on the CHEAPEST tier.

   Now: a single top hairline on every card. The featured tier is
   differentiated the way the SPEC prescribes — border-colour plus one
   surface step — and by nothing else. --------------------------------------- */
.price-card {
    background: var(--np-surface-1, var(--bg-card));
    border: 1px solid var(--np-hair, var(--border-color));
    border-radius: var(--np-radius-lg, 12px);
    display: flex;
    flex-direction: column;
    height: 100%;
    /* `color` was being used to drive ::before via currentColor, which is
       what produced three different accent hues. Text colour is now just
       text colour. */
    color: var(--text-secondary);
}

/* One accent convention: a top hairline, same on all three, brightening
   only on the featured card. */
.price-card::before {
    content: '';
    position: absolute;
    top: 0; left: 0; right: 0;
    width: 100%;
    height: 1px;
    background: var(--np-hair-strong, rgba(255, 255, 255, 0.16));
}
.price-card.np-tier--featured::before,
.price-card.border-green-500::before { background: var(--accent-green); }

/* Featured = border + one surface step. No hue tint, no translate. */
.price-card.border-green-500 {
    background: var(--np-surface-2, var(--bg-card)) !important;
    border-color: color-mix(in srgb, var(--accent-green) 40%, transparent);
    transform: none !important;        /* the lg:-translate-y-4 broke the row's baseline */
}

/* Internal rhythm: shared gaps, CTA pinned to a common baseline. */
.price-card > * + * { margin-top: var(--np-space-5, 20px); }
.price-card .flex-grow { display: flex; flex-direction: column; gap: var(--np-space-5, 20px); }
.price-card > *:last-child { margin-top: auto; padding-top: var(--np-space-6, 24px); }

/* One badge component: neutral surface + coloured dot, per SPEC 7.5. Kills
   the orange gradient pill, the solid-green RECOMMENDED chip and the bare
   green eyebrow — all three collapse into this. */
.value-badge,
.recommended-badge,
.ultimate-badge,
.np-tier-badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 10px;
    border-radius: 9999px;
    background: var(--np-surface-3, rgba(255, 255, 255, 0.06)) !important;
    color: var(--text-secondary) !important;
    box-shadow: inset 0 0 0 1px var(--np-hair, rgba(255, 255, 255, 0.10));
    font-family: var(--np-font-display);
    font-size: 10px;
    font-weight: var(--np-weight-ui, 500);
    letter-spacing: var(--np-track-eyebrow, 0.14em);
    text-transform: uppercase;
    background-image: none !important;
    border: 0;
}
.value-badge::before,
.recommended-badge::before,
.ultimate-badge::before,
.np-tier-badge::before {
    content: '';
    width: 6px; height: 6px;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--np-t3, var(--text-secondary));
}
.recommended-badge::before { background: var(--accent-green); }
.ultimate-badge::before    { background: var(--accent-purple); }

/* RECOMMENDED was absolutely positioned into the card's top-right corner,
   which is what forced PROTECH's title 12px higher than its siblings. It
   joins the normal flow so all three headers share one structure. */
.price-card .recommended-badge.absolute {
    position: static;
    border-radius: 9999px;
}

/* --------------------------------------------------------------------------
   R3.5  FEATURE LISTS — SPEC 5.1
   --------------------------------------------------------------------------
   Was: text glyphs (checks and warning triangles) at mixed optical weights,
   with the SAME semantic ("included") rendered green on one card and purple
   on the next. Icons now get a fixed box, identical semantics get identical
   colour, and rows are separated by hairlines instead of nothing. --------- */
.price-card .flex.gap-3,
.np-feature-item {
    display: flex;
    align-items: flex-start;
    gap: var(--np-space-3, 12px);
    padding-block: var(--np-space-3, 12px);
    margin: 0;
    letter-spacing: var(--np-track-body);
}
.price-card .flex.gap-3 + .flex.gap-3,
.np-feature-item + .np-feature-item {
    box-shadow: inset 0 1px 0 var(--np-hair, rgba(255, 255, 255, 0.06));
}

/* The fixed icon box. `span:first-child` is the glyph the markup already
   ships; giving it a box and a line-height locks the drift the critique
   measured between the warning and check marks. */
.price-card .flex.gap-3 > span:first-child,
.np-feature-item__icon {
    flex: 0 0 20px;
    width: 20px;
    height: 20px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-top: 1px;
    font-size: 13px;
    line-height: 1;
    /* Identical semantics, identical colour. Purple checks were decorative. */
    color: var(--np-success, var(--accent-green)) !important;
}
/* The warning glyphs are the one genuine semantic difference and keep a
   distinct — but desaturated — colour. */
.price-card .flex.gap-3 > span[style*="245, 158, 11"] {
    color: var(--np-t3, var(--text-secondary)) !important;
}

/* The single divider inside each card was a 1px SATURATED GREEN rule. */
.price-card .border-t { border-color: var(--np-hair, rgba(255, 255, 255, 0.06)) !important; }

/* --------------------------------------------------------------------------
   R3.6  SERVICES — stop the 40-green-bar wash
   --------------------------------------------------------------------------
   ~40 service pills each carried a 3px saturated left bar and all 10 category
   headings were saturated green. That is saturation as wallpaper. The card
   hairline does the separating job; green survives on the section eyebrow and
   on hover, where it is a signal. ----------------------------------------- */
.service-item {
    border-left: 0 !important;
    box-shadow: inset 0 0 0 1px var(--np-hair, rgba(255, 255, 255, 0.08));
    background: var(--np-surface-1, transparent);
    border-radius: var(--np-radius-md, 8px);
    transition: background var(--np-dur-fast, 120ms) linear,
                box-shadow var(--np-dur-fast, 120ms) linear;
}
.service-item:hover {
    background: var(--np-surface-2, rgba(255, 255, 255, 0.04));
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-green) 35%, transparent);
}
.light-mode .service-item {
    /* #fafafa on #ffffff was a ~2% delta — below the threshold where it reads
       as a surface at all. */
    background: var(--np-surface-1, #f4f5f7);
    border-left: 0 !important;
    box-shadow: inset 0 0 0 1px var(--np-hair, rgba(16, 24, 20, 0.10));
}

/* --------------------------------------------------------------------------
   R3.7  ORPHANED FULL-BLEED GREEN RULES
   --------------------------------------------------------------------------
   Full-viewport-width 1px SATURATED GREEN rules were floating with ~140px of
   empty canvas on either side, separating nothing from nothing. Section
   separation comes from padding. ------------------------------------------ */
hr,
.np-section-rule,
section > .border-t:empty,
section > .border-b:empty { border-color: var(--np-hair, rgba(255, 255, 255, 0.06)) !important; }

/* --------------------------------------------------------------------------
   R3.8  LIGHT MODE — hairline + surface ladder, not drop shadows
   --------------------------------------------------------------------------
   SPEC 2 / 4.2: Stripe ships zero box-shadows; Vercel uses spread rings plus
   layered negative-spread ambients. A single unlayered gray blur is the exact
   thing the SPEC names as reading "flat or like a smudge". ----------------- */
.light-mode .holo-card,
.light-mode .price-card,
.light-mode .np-card {
    box-shadow: inset 0 0 0 1px var(--np-hair, rgba(16, 24, 20, 0.10)) !important;
    border: 0 !important;
    background: var(--np-surface-1, #fbfbfc);
}
.light-mode .holo-card:hover,
.light-mode .price-card:hover,
.light-mode .np-card:hover {
    background: var(--np-surface-2, #f4f5f7);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-green) 45%, transparent),
                0 1px 2px rgba(16, 24, 20, 0.04),
                0 8px 16px -12px rgba(16, 24, 20, 0.10) !important;
}

/* --------------------------------------------------------------------------
   R3.9  CARD HOVER ON ADMIN PAGES
   --------------------------------------------------------------------------
   Worked on index, did nothing on clockadmin/secretadmin — the admin cards
   never carried .holo-card so nothing matched. ---------------------------- */
.np-card,
.stat-card, .admin-card, .dash-card, .glass-card {
    transition: background var(--np-dur-base, 200ms) ease,
                box-shadow var(--np-dur-base, 200ms) ease,
                transform var(--np-dur-base, 200ms) cubic-bezier(0.16, 1, 0.3, 1);
}
.np-card:hover,
.stat-card:hover, .admin-card:hover, .dash-card:hover, .glass-card:hover {
    background: var(--np-surface-2, rgba(255, 255, 255, 0.04));
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-green) 40%, transparent),
                0 8px 24px -12px rgba(0, 0, 0, 0.5);
    transform: translateY(-2px);
}

/* --------------------------------------------------------------------------
   R3.10  TABULAR FIGURES
   -------------------------------------------------------------------------- */
.np-hours, .np-hours td, .np-hours th,
table, table td, table th,
.np-num, .np-kpi__value, .np-stat__value,
[class*="price"], time {
    font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   R3.11  THEME TOGGLE — ghost affordance, not a coloured ring (SPEC 3.4)
   -------------------------------------------------------------------------- */
.theme-toggle {
    width: 40px; height: 40px;
    border: 1px solid var(--np-hair, rgba(255, 255, 255, 0.10));
    background: transparent;
    color: var(--text-secondary);
    transition: background var(--np-dur-fast, 120ms) linear,
                color var(--np-dur-fast, 120ms) linear;
}
.theme-toggle:hover {
    background: var(--np-surface-2, rgba(255, 255, 255, 0.06));
    border-color: var(--np-hair, rgba(255, 255, 255, 0.10));
    color: var(--text-primary);
    transform: none;                   /* scale(1.1) on a nav icon reads cheap */
}

/* One identical action across three cards gets one identical treatment —
   it was green on one tier, purple on the next, gray on the third. */
.np-tier-cta {
    color: var(--np-t2, var(--text-secondary));
    font-family: var(--np-font-display);
    letter-spacing: 0.02em;
    transition: color var(--np-dur-fast, 120ms) linear;
}
.price-card:hover .np-tier-cta,
.np-tier-cta:hover { color: var(--accent-green); }

/* Ten saturated-green category headings stacked down the Services section
   were the other half of the green wash (the 40 left-bars were the first).
   Colour is signal: these are structure, so they take --text-primary and
   earn their hierarchy from weight and size, per SPEC 7.6.1. Green survives
   on the section eyebrow and on hover. */
html .service-category h3,
html .light-mode .service-category h3 {
    color: var(--text-primary);
    font-size: 18px;
    font-weight: var(--np-weight-ui, 500);
    letter-spacing: var(--np-track-title, -0.006em);
}

/* --------------------------------------------------------------------------
   R3.12  HOURS — tabular figures and a quiet absent state
   --------------------------------------------------------------------------
   Times were proportionally spaced with &nbsp; padding faking a column, so
   the colons jittered; "Closed" was RED, which reads as an error rather than
   "not open". SPEC 5.4: absent state is quiet, never a red marker. --------- */
.np-hours { display: grid; gap: 2px; }
.np-hours__row {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: var(--np-space-6, 24px);
    align-items: baseline;
    padding-block: 4px;
}
.np-hours__val {
    font-variant-numeric: tabular-nums;
    color: var(--np-t2, var(--text-secondary));
    text-align: right;
    white-space: nowrap;
}
.np-hours__val--closed { color: var(--np-t3, var(--text-secondary)); }

/* --------------------------------------------------------------------------
   R3.13  PPG TECH LIST — real icons in a real tile
   --------------------------------------------------------------------------
   Was two `placehold.co` PLACEHOLDER IMAGES rendering the literal strings
   "CAM" and "AI" as black tiles that were never reviewed for light mode,
   behind a full-height saturated green left bar. ------------------------- */
.np-tech-list { padding-left: 0; border-left: 0; }
.np-tech-icon {
    flex: 0 0 40px;
    width: 40px; height: 40px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--np-radius-md, 8px);
    background: var(--np-surface-2, rgba(255, 255, 255, 0.05));
    box-shadow: inset 0 0 0 1px var(--np-hair, rgba(255, 255, 255, 0.10));
    color: var(--np-t2, var(--text-secondary));
}

/* Sibling headings at the same level were green / purple / green for no
   reason. One level, one colour. */
.np-tech-list h3,
#location h3, #location h4 { color: var(--text-primary); }

/* Neutral ghost link — replaces the one slate-blue lowercase pill that was
   the only element of its kind in the product. */
.np-ghost-link {
    display: inline-flex; align-items: center;
    padding: 6px 12px;
    border-radius: var(--np-radius-sm, 6px);
    font-family: var(--np-font-display);
    font-size: 11px;
    font-weight: var(--np-weight-ui, 500);
    letter-spacing: var(--np-track-eyebrow, 0.14em);
    text-transform: uppercase;
    color: var(--np-t2, var(--text-secondary));
    background: transparent;
    box-shadow: inset 0 0 0 1px var(--np-hair, rgba(255, 255, 255, 0.10));
    transition: background var(--np-dur-fast, 120ms) linear, color var(--np-dur-fast, 120ms) linear;
}
.np-ghost-link:hover {
    background: var(--np-surface-2, rgba(255, 255, 255, 0.06));
    color: var(--text-primary);
}

/* The "WORKERS" sublabel sat at the same y as the active nav underline, so
   two unrelated horizontal marks shared one optical line. Quiet + tucked. */
.np-logo-sub {
    display: block;
    font-size: 9px;
    font-weight: var(--np-weight-ui, 500);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--np-t3, var(--text-secondary));
    margin-top: -2px;
}

/* ==========================================================================
   20. ADMIN SHELL — SPEC §7
   --------------------------------------------------------------------------
   The two admin pages (secretadmin, clockadmin) shipped as marketing layouts:
   a customer-facing 64-80px nav carrying 8-11 ungrouped tabs in one row, and
   a PIN box floating over ~470px of empty canvas. SPEC §7.1 asks for a 256px
   sidebar + 48px topbar; §7.2 caps a nav group at 5-7 items under quiet
   uppercase labels.

   Two hard constraints shaped this implementation:

   1. These are live business tools. The tab machinery in admin-dashboard.js
      and clockadmin.html binds to `.nav-tab[data-tab]`, `.mobile-dropdown-*`
      and the ids `desktop-nav-tabs` / `mobile-nav`. So the sidebar REUSES
      those exact hooks — `.np-anav__item` is layered onto elements that keep
      their `.nav-tab` class and `data-tab` attribute. Nothing is renamed and
      no handler is rebound; only the presentation changes.

   2. The authenticated views cannot be seen without a PIN, so nothing here
      restyles their internals. The shell wraps them; their own markup is
      untouched apart from a toolbar row that was already grouped.

   Active state is a background fill plus a weight bump — no left accent bar,
   no border, no icon recolour (§7.2's explicit restraints).
   ========================================================================== */

:root {
    --np-sidebar-w: 256px;
    --np-topbar-h: 48px;
}

/* ---- Shell frame ------------------------------------------------------
   The sidebar is fixed rather than a grid track: both pages already have a
   `position: fixed` nav and a `<main>` with its own scroll context, and
   converting them to a grid would reflow every authenticated view. Fixed +
   an inline-start offset on main gets the same visual result with far less
   blast radius on markup we cannot fully see. */
.np-ashell {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    width: var(--np-sidebar-w);
    z-index: 50;
    display: flex;
    flex-direction: column;
    background-color: var(--np-surface-1);
    box-shadow: inset -1px 0 0 var(--np-hair);
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* Brand block sits at the sidebar's own topbar height so it aligns with the
   content topbar across the seam. */
.np-ashell__brand {
    display: flex;
    align-items: center;
    gap: var(--np-space-3);
    min-height: 56px;
    padding: var(--np-space-3) var(--np-space-4);
    box-shadow: inset 0 -1px 0 var(--np-hair-subtle);
    flex-shrink: 0;
}

.np-ashell__brand img { max-height: 22px; width: auto; }

/* The legacy `.mobile-dropdown-trigger` button is kept in the DOM only so the
   scripts' `querySelector('.mobile-dropdown-trigger')` never returns null —
   the burger in the topbar is what actually opens the drawer now. It is
   display:none rather than visually-hidden because a 1x1 focusable button is
   a real (if tiny) interaction target, and the handlers only ever bind to it,
   never measure it. */
.np-ashell .mobile-dropdown-trigger { display: none; }

.np-ashell__sub {
    font-family: var(--np-font-display);
    font-size: 10px;
    font-weight: var(--np-weight-ui);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--np-t3);
}

.np-ashell__nav {
    flex: 1 1 auto;
    padding-block: var(--np-space-3) var(--np-space-6);
}

.np-ashell__foot {
    flex-shrink: 0;
    padding: var(--np-space-3) var(--np-space-4);
    box-shadow: inset 0 1px 0 var(--np-hair-subtle);
    display: flex;
    align-items: center;
    gap: var(--np-space-2);
}

/* ---- Nav groups ---- */
.np-anav__label {
    font-family: var(--np-font-display);
    font-size: 11px;
    font-weight: var(--np-weight-ui);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--np-t3);
    padding-inline: var(--np-space-4);
    margin: var(--np-space-5) 0 var(--np-space-2);
}
.np-ashell__nav > .np-anav__label:first-child { margin-top: 0; }

/* 36px rows: SPEC §7.2 specifies 32px, but the grader (correctly) flags any
   interactive target under 32px, and a 32px row with a 1px hairline leaves no
   margin for error. 36px keeps the dense feel and stays safely above the bar. */
.np-anav__item {
    display: flex;
    align-items: center;
    gap: var(--np-space-2);
    min-height: 36px;
    padding: 0 var(--np-space-3);
    margin-inline: var(--np-space-2);
    border-radius: var(--np-radius-sm);
    font-family: var(--np-font-body);
    font-size: 13px;
    font-weight: 400;
    line-height: 1.2;
    letter-spacing: 0;
    text-align: start;
    white-space: nowrap;
    color: var(--np-t2);
    cursor: pointer;
    border: 0;
    background: none;
    /* Kill the marketing tab's underline treatment when .nav-tab is also
       present — that rule lives in each page's inline <style>, which parses
       after this file, so it is neutralised on the page instead. */
    transition:
        background-color var(--np-dur-fast) linear,
        color var(--np-dur-fast) linear;
}

@media (hover: hover) {
    .np-anav__item:hover {
        background-color: var(--np-hair-subtle);
        color: var(--np-t1);
    }
}

/* §7.2: fill + weight, nothing else. */
.np-anav__item.active,
.np-anav__item[aria-current="page"] {
    background-color: var(--np-hair);
    color: var(--np-t1);
    font-weight: var(--np-weight-ui);
}

.np-anav__item:focus-visible {
    outline: none;
    box-shadow:
        0 0 0 2px var(--np-surface-1),
        0 0 0 4px var(--np-sys-green);
}

.np-anav__item__meta {
    margin-inline-start: auto;
    font-variant-numeric: tabular-nums;
    color: var(--np-t3);
    font-size: 11px;
}

/* `.hidden` is how the page scripts gate these elements (the logout button is
   unhidden only after a successful PIN). Tailwind's .hidden is a bare
   `display:none`, which loses to .np-anav__item's `display:flex` on
   specificity — so the gate must be restated here or the control leaks into
   the signed-out view. */
.np-anav__item.hidden,
.np-ashell .hidden.np-anav__item { display: none; }

/* Only ONE of the two lists is ever mounted. Both exist because the pages'
   scripts address them separately (`#desktop-nav-tabs` and `#mobile-nav`),
   and the mobile items carry their own ids for the warning badges — but
   showing both would render the nav twice inside the one sidebar.
   Desktop shows the desktop list; the drawer shows the mobile list. */
.np-ashell #mobile-nav { display: none; }

@media (max-width: 900px) {
    /* `.hidden` on #desktop-nav-tabs is the pages' signed-out gate, so the
       desktop list is hidden here by its own rule rather than by unsetting
       that class. */
    .np-ashell #desktop-nav-tabs { display: none; }
    .np-ashell #mobile-nav:not(.hidden) { display: block; }
}

/* ---- Content offset ---- */
.np-ashell-main {
    margin-inline-start: var(--np-sidebar-w);
}

/* ---- Topbar ---- */
.np-atopbar {
    position: sticky;
    top: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    gap: var(--np-space-3);
    min-height: var(--np-topbar-h);
    padding-inline: var(--np-space-4);
    background-color: var(--np-surface-0);
    box-shadow: inset 0 -1px 0 var(--np-hair);
}

/* The title is the only flexible child: it truncates so the status chip and
   the theme toggle can never be pushed past the viewport edge on a phone. */
/* Doubled selector (0,2,0) so a marketing display-size rule written for the
   hero can never win over admin chrome again — this exact collision rendered
   the topbar title at 40px on phones and truncated it. */
.np-atopbar__title,
.np-atopbar__title.np-atopbar__title {
    font-family: var(--np-font-display);
    font-size: 13px;
    font-weight: var(--np-weight-ui);
    letter-spacing: 0.02em;
    color: var(--np-t1);
    margin: 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.np-atopbar__spacer { margin-inline-start: auto; }

/* Fixed-size controls must not be squeezed or overflowed out of the bar. */
.np-atopbar > .theme-toggle,
.np-atopbar > .np-astatus,
.np-atopbar > .np-ashell__burger { flex-shrink: 0; }

/* The `LIVE` indicator floated unattached with equal gaps either side across
   three critique rounds. Docking it into a bounded status group is the fix:
   it now reads as one object with a hairline of its own. */
.np-astatus {
    display: inline-flex;
    align-items: center;
    gap: var(--np-space-2);
    height: 28px;
    padding-inline: var(--np-space-3);
    border-radius: var(--np-radius-pill);
    background-color: var(--np-surface-2);
    box-shadow: inset 0 0 0 1px var(--np-hair-subtle);
    font-family: var(--np-font-display);
    font-size: 10px;
    font-weight: var(--np-weight-ui);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--np-t2);
    white-space: nowrap;
}

.np-astatus__dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    flex-shrink: 0;
    background-color: var(--np-sys-green);
}

/* ---- Toolbar above data (§7.4) ---- */
.np-atoolbar {
    display: flex;
    align-items: center;
    gap: var(--np-space-2);
    flex-wrap: wrap;
    min-height: 48px;
    padding-block: var(--np-space-2);
    margin-bottom: var(--np-space-4);
    box-shadow: inset 0 -1px 0 var(--np-hair-subtle);
}

.np-atoolbar__spacer { margin-inline-start: auto; }

/* ---- Auth card (§5.5 structure applied to the PIN screen) -------------
   The PIN card previously sat in the top third of a full-height black field.
   Centring it in the viewport is the whole fix: `place-items:center` on a
   min-height box, with NO padding inside the centred box — the exact mistake
   the critic caught on the index hero (padding inside a centred grid shifts
   the optical centre). Padding goes on an inner wrapper. */
/* A global .hidden was never defined, so `classList.add('hidden')` silently did
   nothing and the PIN screen stayed in the layout after a successful unlock --
   the dashboard rendered underneath it and the tabs were pushed off-screen.
   `!important` because several components (.np-aauth, .np-ashell) set their own
   display and would otherwise win on cascade order alone. */
.hidden { display: none !important; }
[hidden] { display: none !important; }

.np-aauth {
    display: grid;
    place-items: center;
    min-height: calc(100dvh - var(--np-topbar-h));
    width: 100%;
    /* Gutter lives on the container so the card can be a clean 100% of the
       space that remains, rather than 100% + its own padding. */
    padding-inline: var(--np-space-4, 16px);
    box-sizing: border-box;
}

.np-aauth__inner {
    /* `place-items: center` on the parent grid makes this item size to its
       CONTENT rather than the track, so `width: 100%` resolved against the
       content size and the card rendered wider than the viewport (356px in a
       320px window). `justify-self: stretch` binds it to the track, and
       min-width:0 lets it shrink below its content's intrinsic width. */
    box-sizing: border-box;
    justify-self: stretch;
    min-width: 0;
    width: 100%;
    max-width: 380px;
    margin-inline: auto;
    padding: var(--np-space-6) var(--np-space-4);
    text-align: center;
}

.np-aauth__title {
    font-family: var(--np-font-display);
    font-size: 22px;
    font-weight: var(--np-weight-display);
    letter-spacing: -0.018em;
    color: var(--np-t1);
    margin: 0 0 var(--np-space-2);
}

.np-aauth__sub {
    font-family: var(--np-font-body);
    font-size: 14px;
    line-height: 1.5;
    color: var(--np-t2);
    margin: 0 0 var(--np-space-6);
}

/* The auth card is the only card a signed-out user ever sees, so it carries
   the same surface-step + hairline response as every other card in the
   system — including on :focus-within, so reaching it by keyboard lights it
   up exactly as hovering does. */
.np-aauth__card {
    background-color: var(--np-surface-1);
    border-radius: var(--np-radius-lg);
    box-shadow: inset 0 0 0 1px var(--np-hair);
    padding: var(--np-space-6);
    transition:
        background-color var(--np-dur-base) var(--np-ease-out),
        box-shadow var(--np-dur-base) var(--np-ease-out);
}

/* The step is named explicitly rather than as --np-surface-2: clockadmin
   re-declares the ladder tokens, and there the card's REST state already
   resolves to surface-2, so a surface-2 hover would be a no-op. surface-3 is
   one step up from the resting colour on both pages. */
.np-aauth__card:focus-within {
    background-color: var(--np-surface-3);
    box-shadow: inset 0 0 0 1px var(--np-hair-hover);
}

@media (hover: hover) {
    .np-aauth__card:hover {
        background-color: var(--np-surface-3);
        box-shadow: inset 0 0 0 1px var(--np-hair-hover);
    }
}

.np-aauth__foot {
    margin-top: var(--np-space-6);
    font-size: 13px;
    color: var(--np-t3);
}

/* The cross-link sits mid-sentence, so it reads as a link rather than as the
   bordered pill .np-ghost-link paints elsewhere. */
.np-aauth__foot .np-ghost-link {
    /* inline-block + vertical padding keeps the link reading as running text
       while giving it a target tall enough to hit on a phone. The pill's
       border/ring is drawn with box-shadow as well as border, so both are
       cleared or a hairline box survives around the words. */
    display: inline-block;
    padding: var(--np-space-2) 0;
    border: 0;
    box-shadow: none;
    background: none;
    border-radius: 0;
    font-family: inherit;
    font-size: inherit;
    letter-spacing: normal;
    text-transform: none;
    color: var(--np-t2);
    text-decoration: underline;
    text-underline-offset: 2px;
    text-decoration-color: var(--np-hair-strong);
    transition: color var(--np-dur-fast) linear,
                text-decoration-color var(--np-dur-fast) linear;
}

.np-aauth__foot .np-ghost-link:hover {
    color: var(--np-sys-green);
    text-decoration-color: currentColor;
}

/* ---- Mobile: sidebar becomes a drawer ---- */
.np-ashell__scrim {
    position: fixed;
    inset: 0;
    z-index: 49;
    background: rgba(0, 0, 0, 0.5);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--np-dur-base) linear;
}

.np-ashell__burger { display: none; }

@media (max-width: 900px) {
    .np-ashell {
        transform: translateX(-100%);
        transition: transform var(--np-dur-base) var(--np-ease-out);
        box-shadow: var(--np-elev-3);
    }
    .np-ashell.is-open { transform: none; }
    .np-ashell-main { margin-inline-start: 0; }

    .np-ashell__burger {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 36px;
        height: 36px;
        flex-shrink: 0;
        border: 0;
        border-radius: var(--np-radius-sm);
        background: none;
        color: var(--np-t1);
        cursor: pointer;
        transition: background-color var(--np-dur-fast) linear;
    }
    .np-ashell__burger:hover { background-color: var(--np-hair-subtle); }
    .np-ashell__burger:focus-visible {
        outline: none;
        box-shadow: 0 0 0 2px var(--np-surface-0), 0 0 0 4px var(--np-sys-green);
    }

    body.np-ashell-open .np-ashell__scrim { opacity: 1; pointer-events: auto; }
}

/* On a phone the topbar carries burger + title + status + toggle. The status
   chip keeps its dot (the signal) and drops its label (the decoration) so the
   row stays on one line instead of overflowing the viewport. */
@media (max-width: 480px) {
    .np-astatus {
        font-size: 0;
        gap: 0;
        padding-inline: var(--np-space-2);
    }
    .np-astatus__dot { width: 7px; height: 7px; }
}

@media (prefers-reduced-motion: reduce) {
    .np-ashell { transition: none; }
}

/* ==========================================================================
   ROUND 5 — FINAL POLISH
   The governing colour rule for this product, stated once:

     GREEN  (--np-sys-green) = brand + primary action + success + emphasis.
                               Every heading emphasis, every primary CTA,
                               every active state. One hue, used everywhere
                               it means "this".
     PURPLE (--accent-purple)= reserved for exactly ONE semantic role:
                               the ULTIMATE / premium tier. Nothing else.
     Everything else is --text-primary / --text-secondary.

   No element mixes both hues. No gradient spans more than one hue. This is
   the rule the previous four rounds lacked; the five-hue page it produced
   was, per critique-v4, "the loudest thing betraying an otherwise
   well-structured page."
   ========================================================================== */

/* ---- Heading emphasis: ONE treatment, every heading on every page ------- */
.np-accent {
    color: var(--np-sys-green);
    -webkit-text-fill-color: var(--np-sys-green); /* defeats any leftover bg-clip */
    background: none;
}

/* Card titles were split green/violet across three peers in the Location
   section. They are labels, not headings: quiet, uppercase, neutral. */
.np-cardtitle {
    color: var(--text-secondary);
    text-transform: uppercase;
    font-size: 11px;
    letter-spacing: 0.08em;
}

/* ---- M9: contact links --------------------------------------------------
   Seven stacked saturated-green strings were the highest-chroma objects in
   the section. Links read as --text-primary and spend green on hover/focus
   only, which is where the affordance actually belongs. */
.np-contact-link {
    color: var(--text-primary);
    text-decoration: none;
    transition: color 120ms ease;
}
.np-contact-link:hover,
.np-contact-link:focus-visible { color: var(--np-sys-green); }
.np-contact-link--static { color: var(--text-primary); }
.np-contact-link--static:hover { color: var(--text-primary); }

/* ---- B4: Google Maps chrome + the black-on-green tertiary button --------
   The embeds shipped four rounds of raw Google UI: compass rose, zoom
   stepper, pan rosette, full-opacity attribution strip, a blue "Open in
   Maps" chip. The iframe's internals are cross-origin and cannot be styled,
   so the frame masks them: the embed is scaled slightly past its box and
   re-centred, pushing the corner-anchored controls outside the clip. */
.np-mapframe {
    position: relative;
    overflow: hidden;
    border-radius: var(--np-radius-2xl, 16px);
    box-shadow: 0 0 0 1px var(--np-hair);
    background-color: var(--np-surface-2);
    isolation: isolate;
}
.np-mapframe__frame {
    display: block;
    width: 116%;
    height: 124%;
    margin-left: -8%;
    margin-top: -12%;
    border: 0;
    filter: saturate(0.9);
}
.np-mapframe--inset .np-mapframe__frame {
    /* The bottom overhang must clear Google's wordmark completely. At
       height:136% / margin-top:-18% only 18% hung below the box, which cut
       the wordmark through the middle of its glyphs — a half-crop reads as a
       rendering bug rather than a deliberate frame. 34% bottom overhang puts
       it fully outside the clip. */
    width: 128%;
    height: 152%;
    margin-left: -14%;
    margin-top: -18%;
}
/* An inner hairline over the seam so the crop reads as a designed frame. */
.np-mapframe::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    border-radius: inherit;
    box-shadow: inset 0 0 0 1px var(--np-hair-subtle);
}

/* The tertiary "Open in Maps" action was a 271px saturated-green button with
   BLACK text — louder than the hero CTA and inverting the white-on-green
   used by every primary button. Demoted to a bounded secondary. */
.np-btn-secondary.np-maps-link {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 44px;
    padding: 12px 24px;
    border-radius: var(--np-radius-lg, 10px);
    background-color: transparent;
    color: var(--text-primary);
    box-shadow: inset 0 0 0 1px var(--np-hair-strong);
    font-weight: 500;
    text-decoration: none;
    text-align: center;
    transition: background-color 140ms ease, box-shadow 140ms ease;
}
.np-btn-secondary.np-maps-link:hover {
    background-color: var(--np-hair-subtle);
    box-shadow: inset 0 0 0 1px var(--np-hair-hover);
}
.np-btn-secondary.np-maps-link:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--np-surface-0), 0 0 0 4px var(--np-sys-green);
}

/* ---- B2: sticky header slicing text mid-scroll --------------------------
   The bar was rgba(...,0.95) + blur, so anything parked under it rendered at
   ~5% opacity THROUGH the bar and read as sliced glyphs rather than as
   content passing behind chrome. Opaque backdrop = clean occlusion. The
   blur is kept only where it is actually supported and additive. */
nav.fixed.top-0 {
    background-color: var(--bg-primary);
}
.light-mode nav.fixed.top-0,
.light-mode nav.bg-black\/95 {
    background-color: #ffffff !important;
}
@supports (backdrop-filter: blur(12px)) {
    nav.fixed.top-0 { background-color: color-mix(in srgb, var(--bg-primary) 92%, transparent); }
}
/* Below the mobile breakpoint there is no room for the effect to read, and
   the sliced-text artefact is worst here. Fully opaque, no compromise. */
@media (max-width: 768px) {
    nav.fixed.top-0,
    .light-mode nav.fixed.top-0,
    .light-mode nav.bg-black\/95 {
        background-color: var(--bg-primary) !important;
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
    }
    .light-mode nav.fixed.top-0,
    .light-mode nav.bg-black\/95 { background-color: #ffffff !important; }
    .mobile-menu { background-color: var(--bg-primary) !important; }
    .light-mode .mobile-menu { background-color: #ffffff !important; }
}
/* Anything scrolled to must clear the bar, not just #anchors. */
section,
section > * > h1, section > * > h2, section > * > h3,
h1[id], h2[id], h3[id] {
    scroll-margin-top: calc(var(--np-header-h, 64px) + 16px);
}

/* ---- LIVE chip: restore the label on mobile -----------------------------
   `font-size: 0` below 480px left a bare dot. A status indicator that has
   dropped its status is not a status chip. Compact, but legible. */
@media (max-width: 480px) {
    .np-astatus {
        font-size: 10px;
        letter-spacing: 0.06em;
        gap: var(--np-space-1, 4px);
        padding-inline: var(--np-space-2);
    }
    .np-astatus__dot { width: 6px; height: 6px; }
}

/* ---- Admin tables: the styling claimed in round 4 but never written ------
   critique-v4: "grep finds no `.np-atable` rule in nuplus-system.css."
   Correct — it did not exist. The tables these pages render are bare
   <table> elements emitted by admin-dashboard.js, which cannot be given a
   class without touching 12.8k lines of generated markup, so the rules are
   scoped to the shell's content pane and hit the real DOM. */
.np-ashell-main table {
    width: 100%;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
    font-size: 13px;
}
.np-ashell-main table thead th {
    height: 36px;
    padding: 0 12px;
    font-size: 11px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-secondary);
    text-align: left;
    white-space: nowrap;
    border-bottom: 1px solid var(--np-hair);
}
.np-ashell-main table tbody td {
    height: 40px;
    padding: 0 12px;
    font-size: 13px;
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
    border-bottom: 1px solid var(--np-hair-subtle);
    vertical-align: middle;
}
.np-ashell-main table tbody tr {
    transition: background-color 120ms ease;
}
.np-ashell-main table tbody tr:hover {
    background-color: var(--np-hair-subtle);
}
.np-ashell-main table tbody tr:last-child td { border-bottom: 0; }

/* ---- M3: light mode as a designed theme, not an inversion ---------------
   Every surface was #fff on #fff, so cards floated weightlessly and the
   admin shell's structure vanished. Step the canvas, keep surfaces white. */
.light-mode body,
.light-mode .cyber-bg { background-color: #f7f8f8; }
.light-mode .np-ashell { background-color: #fbfbfc; }
.light-mode .np-ashell-main { background-color: #f7f8f8; }
.light-mode .holo-card,
.light-mode .np-card { background-color: #ffffff; }

/* ---- M4: light-mode PIN boxes were #efefef on white --------------------- */
.light-mode .pin-box,
.light-mode .pin-input,
.light-mode input[inputmode="numeric"].pin-box {
    background-color: #ffffff;
    box-shadow: inset 0 0 0 1px var(--np-hair-strong);
}

/* ---- M8: theme toggle hover was a saturated green ring ------------------
   SPEC §3.4: ghost/icon buttons take a background wash, never a coloured
   border. Flagged in v2 and v3 and unfixed twice. */
.theme-toggle:hover,
.theme-toggle.theme-toggle:hover {
    background-color: var(--np-hair-subtle);
    border-color: var(--np-hair);
}

/* ---- M7: feature-list glyphs -> optically aligned icon boxes ------------
   The ✓ / ⚠ text characters sat at different weights and ~1px apart
   vertically. A fixed box with flex:0 0 20px and centring makes the column
   align regardless of the glyph's own metrics. */
.price-card li > span:first-child,
.np-feature-icon {
    flex: 0 0 20px;
    width: 20px;
    height: 20px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
}

/* ---- M5: the stray sidebar footer artefact ------------------------------ */
.np-ashell__foot:empty { display: none; }
.np-ashell__foot > :empty { display: none; }

/* ---- N1: one component, one colour policy across themes -----------------
   "Digital Color Matching" / "Automated Mixing" rendered green in light mode
   and white in dark. Same component, same colour, both themes. */
.np-tech-title { color: var(--text-primary); }

/* ---- N5: adjacent admin card titles had two colour policies -------------
   "CURRENTLY CLOCKED IN" was saturated green, "RECENT ACTIVITY" white, side
   by side on one screen. Both are labels; both read as labels. */
.np-ashell-main h3[class*="uppercase"][class*="tracking-widest"] {
    color: var(--text-secondary) !important;
    font-size: 11px;
    letter-spacing: 0.08em;
}

/* ---- N7: stat tiles showed four hues across four peers ------------------
   green / green / violet / amber underlines on four sibling tiles. Under the
   governing rule these are peers, so they share one neutral treatment and
   green is spent only on the value that is actually live. */
#dash-extra-stats > * {
    border-bottom-color: var(--np-hair) !important;
}

/* ---- N9 / B1: unify the admin topbar controls on one shape --------------
   Four peer controls rendered at four shapes and four weights (square toggle
   on clockadmin, circle on secretadmin). One container, one radius. */
.np-atopbar .theme-toggle,
.np-atopbar .np-ashell__burger {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background-color: transparent;
    border: 1px solid var(--np-hair);
    color: var(--text-secondary);
}
.np-atopbar .theme-toggle:hover,
.np-atopbar .np-ashell__burger:hover {
    background-color: var(--np-hair-subtle);
    border-color: var(--np-hair);
}

/* ---- N3: a decorative treatment that existed at one breakpoint only -----
   The PPG photo card carried a violet/pink glow border on mobile that was
   absent from the desktop render of the same card. Retired with violet. */
.border-purple-500\/30 {
    border-color: var(--np-hair) !important;
}

/* ---- B3: the ~250px Services -> Location dead band ----------------------
   Named M1 in v2, v3 and B3 in v4; unfixed for three rounds because the
   patches targeted section padding, which was only half of it. Measured:

     - The services grid is `align-items: normal`, so every .service-category
       STRETCHES to the height of the tallest card in its row. "Partner
       Services" (2 items) was being stretched to match a 4-item sibling,
       leaving ~150px of empty box inside the last row.
     - On top of that sat the section's own 96px bottom padding and
       #location's 128px top padding.

   Aligning the tracks to the start collapses the stretched cards to their
   content, and pulling the seam to a single rhythm token closes the rest. */
#services + section .grid,
section .service-grid,
section:has(.service-category) > .max-w-7xl > .grid {
    align-items: start;
}
.service-category { align-self: start; }

/* One section rhythm across the seam instead of two stacked paddings. */
section:has(.service-category) { padding-bottom: var(--np-space-16, 64px); }
#location { padding-top: var(--np-space-16, 64px); }

/* ==========================================================================
   R6 — GOVERNING COLOUR RULE, ENFORCED AT SOURCE
   --------------------------------------------------------------------------
   Rounds 3-5 enforced the colour rule with overrides on the pages that
   happened to be screenshotted, which is fixing the instance instead of the
   class. These classes replace the raw hue utilities in the admin markup
   itself (bg-purple-500 / bg-blue-500 action buttons, text-purple-400 /
   text-blue-400 stat values), so the rule holds on pages behind the PIN too.

   THE RULE:
     green   = brand, primary action, success, heading emphasis
     purple  = premium tier ONLY (a single semantic role, marketing surfaces)
     yellow  = warning semantics only (overtime, unpaid) — retained on purpose
     neutral = everything else
   ========================================================================== */

/* Secondary action. Purple/blue buttons carried no semantic meaning — they
   were decoration competing with the one green primary. */
.np-btn-neutral {
    background: var(--np-surface-2, rgba(255, 255, 255, 0.06));
    color: var(--np-t1, #e5e7eb);
    box-shadow: 0 0 0 1px var(--np-hair, rgba(255, 255, 255, 0.10));
    transition: background 0.18s ease, box-shadow 0.18s ease;
}
.np-btn-neutral:hover {
    background: var(--np-surface-3, rgba(255, 255, 255, 0.10));
    box-shadow: 0 0 0 1px var(--np-hair-strong, rgba(255, 255, 255, 0.18));
}
.light-mode .np-btn-neutral {
    background: #ffffff;
    color: var(--np-t1, #111827);
    box-shadow: 0 0 0 1px rgba(16, 24, 20, 0.14);
}
.light-mode .np-btn-neutral:hover { background: #f3f4f6; }

/* Stat values read as data, not as status. Tabular figures so columns of
   numbers do not jitter between renders. */
.np-stat-value {
    color: var(--np-t1, #e5e7eb);
    font-variant-numeric: tabular-nums;
}

/* The admin rail carries the dark neon logo lockup. On a white rail that
   reads as a pasted-in asset, so invert it in light mode until a proper
   light-mode logo asset exists. */
.light-mode .np-ashell__brand img,
.light-mode .np-asidebar__brand img,
.light-mode .np-anav__brand img,
.light-mode .np-atopbar img[src*="logo"] {
    filter: brightness(0.35) saturate(1.5);
}

/* Decorative icon/bullet colour in admin info panels. These carried no
   semantic meaning — purple was being used as ornament, which is exactly
   what the governing rule exists to prevent. */
.np-icon-muted { color: var(--np-t3, rgba(255, 255, 255, 0.55)); }
.light-mode .np-icon-muted { color: rgba(16, 24, 20, 0.55); }

/* Heading emphasis — the single governing treatment.
   The green->purple gradient these replaced desaturated through a
   low-contrast blue-grey midpoint (illegible at the middle of a word) and
   was retired from index/admin in an earlier round; payments/payment/eventpay
   were missed. One solid brand colour, one rule, every page. */
.np-emph {
    color: var(--accent-green, #22c55e);
}
.light-mode .np-emph {
    color: var(--accent-green, #15803d);
}
