/*
 * AKSuite design system - see public_html/DESIGN.md for the full
 * spec this file implements. Rewritten 2026-09-03 for the Suite's
 * design-direction reset (utility-software aesthetic: Linear/Vercel/
 * Raycast/Stripe as reference). Superseded entirely: the old
 * CommonCrawl-inspired palette, translucent surface tints, and the
 * DNS tool's per-record-category accent-color system - none of that
 * survives here. History of why the OLD system looked the way it did
 * lives in CLAUDE.md, not repeated in comments here going forward.
 *
 * Tokens are plain CSS custom properties (DESIGN.md section 1),
 * consumed directly by this file's own components AND re-pointed onto
 * Bootstrap's own semantic variables below so existing Bootstrap
 * components (buttons, badges, forms) inherit the right colors
 * without every view needing an immediate rewrite.
 */

:root {
    /* Tells the browser which native-UI palette to use for controls
       this app's own CSS can't restyle directly - a <select>'s own
       open dropdown POPUP (not the closed box, which the .form-select
       rule below already colors correctly via background-color) is
       browser/OS-native chrome, painted according to color-scheme,
       not any background-color rule. Left unset, it defaults to
       'normal' (confirmed live via computed style - see the dated
       fix note on [data-theme="dark"]'s own color-scheme below) and
       the browser falls back to ITS OWN dark/light preference instead
       of this site's - so a visitor whose OS/browser prefers dark but
       who has this site's own theme explicitly set to light (or vice
       versa) got a dropdown popup in the WRONG theme, confirmed live
       (2026-09-09, user-reported: "dropdown background... in light
       mode is dark"). Every native control this affects (select
       popups, checkboxes, scrollbars, etc.) now correctly follows
       data-theme, this app's own resolved theme, not the OS's.
       [data-theme="dark"] below sets the dark counterpart - together
       these two rules are the fix, not a full redesign. */
    color-scheme: light;

    /* Light-mode visual refinement, corrected (2026-09-04, user-
       directed). First pass set --background to pure #FFFFFF, matching
       --surface exactly - user caught this immediately, live, as the
       wrong move: page and card became visually the same color, so
       the ONLY thing separating a card from the page was a 1px border
       - too subtle to read as real depth/hierarchy at a glance,
       "flat" for a different reason than the original too-gray
       complaint. Corrected per explicit direction: PAGE gets a subtle
       cool-gray tint (#F8FAFC, distinct from a plain gray - has a
       slight blue lean, matching this palette's own Gray/Slate
       family rather than a neutral gray that would read as "old
       admin dashboard"), CARD stays pure #FFFFFF - the visible-but-
       very-subtle page/card gap is the actual hierarchy signal now,
       same as Linear/Vercel/Stripe's own convention, not "make
       everything the same white" or "make everything gray". The
       stronger text tokens from the first pass (--text-primary/
       -secondary/-muted/-disabled) are correct and unchanged - that
       part of the original fix stands. --accent/--success/--warning/
       --danger were already an exact match to the brief - untouched
       throughout. Dark mode ([data-theme="dark"]) is explicitly the
       benchmark, never touched. */
    --background: #F8FAFC;
    --surface: #FFFFFF;
    --surface-subtle: #F1F5F9;
    --surface-hover: #F3F4F6;

    --border: #E5E7EB;
    --border-subtle: #EEF0F2;

    --text-primary: #111827;
    --text-secondary: #374151;
    --text-muted: #6B7280;
    --text-disabled: #9CA3AF;

    --accent: #2563EB;
    --accent-hover: #1D4ED8;
    --accent-subtle: #EFF6FF;
    --accent-border: #BFDBFE;
    --accent-rgb: 37, 99, 235;

    --success: #16A34A;
    --success-subtle: rgba(22, 163, 74, 0.14);
    --warning: #D97706;
    --warning-subtle: rgba(217, 119, 6, 0.14);
    --danger: #DC2626;
    --danger-subtle: rgba(220, 38, 38, 0.14);
    --info: #2563EB;
    --info-subtle: rgba(37, 99, 235, 0.14);

    --radius-sm: 6px;
    --radius: 8px;
    --radius-lg: 10px;
    /* Strengthened 2026-09-13 (light mode only, user-directed) - the
       original single-layer 1px/2px/4% shadow relied partly on the
       page/card background gap to read as depth, but that gap sits
       near the BRIGHT end of the brightness range (page #F8FAFC, card
       #FFFFFF) where human contrast sensitivity is weakest - the same
       nominal jump reads far more strongly in dark mode purely because
       it sits near the dark end instead (already documented once in
       this file for border colors - the halation/irradiation effect).
       Rather than re-opening the page/card color pair again, added a
       second, wider/softer shadow layer so the card's depth cue no
       longer depends on that weak-contrast background gap at all.
       [data-theme="dark"] below restores the original single-layer
       value verbatim, since dark mode's own background contrast was
       already doing the job correctly. */
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04), 0 3px 8px -3px rgba(0, 0, 0, 0.08);
    --shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
    --ease: cubic-bezier(0.4, 0, 0.2, 1);
    --duration: 140ms;

    --font-ui: "Geist", "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --font-mono: "Geist Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

    /* Bootstrap bridge - re-point stock Bootstrap variables at the
       tokens above so unmodified Bootstrap components render correctly. */
    --bs-body-bg: var(--background);
    --bs-body-color: var(--text-primary);
    --bs-emphasis-color: var(--text-primary);
    --bs-secondary-color: var(--text-secondary);
    --bs-tertiary-color: var(--text-muted);
    --bs-border-color: var(--border);
    --bs-border-color-translucent: var(--border-subtle);
    --bs-tertiary-bg: var(--surface);
    --bs-border-radius: var(--radius);
    --bs-border-radius-sm: var(--radius-sm);
    --bs-border-radius-lg: var(--radius-lg);
    --bs-primary: var(--accent);
    --bs-primary-rgb: var(--accent-rgb);
    /* badge_class() (app/helpers.php) emits Bootstrap's own
       bg-{color}-subtle/text-{color}-emphasis/border-{color}-subtle
       utilities - confirmed via Bootstrap 5.3.3's real shipped CSS
       that these are separate, pre-computed per-theme literals, NOT
       derived from --bs-success/-warning/-danger/-info at runtime (the
       same "stock Bootstrap primary-bg-subtle ignores a customized
       --bs-primary" trap already documented above for --af-info-*) -
       so overriding the base color alone would leave every status
       badge on stock Bootstrap green/amber/red. Pointed at this app's
       own tokens directly instead, one override each, correct in both
       themes for free since --success-subtle etc. already differ by
       theme (2026-09-04, user-reported: SSL Checker's badges read as
       "default Bootstrap colors", confirmed accurate). */
    --bs-success-bg-subtle: var(--success-subtle);
    --bs-success-border-subtle: var(--success-subtle);
    --bs-success-text-emphasis: var(--success);
    --bs-warning-bg-subtle: var(--warning-subtle);
    --bs-warning-border-subtle: var(--warning-subtle);
    --bs-warning-text-emphasis: var(--warning);
    --bs-danger-bg-subtle: var(--danger-subtle);
    --bs-danger-border-subtle: var(--danger-subtle);
    --bs-danger-text-emphasis: var(--danger);
    --bs-info-bg-subtle: var(--info-subtle);
    --bs-info-border-subtle: var(--info-subtle);
    --bs-info-text-emphasis: var(--info);
    --bs-link-color: var(--accent);
    --bs-link-hover-color: var(--accent-hover);
    --bs-font-sans-serif: var(--font-ui);
    --bs-font-monospace: var(--font-mono);
}

[data-theme="dark"] {
    color-scheme: dark;

    --background: #0B0D0F;
    --surface: #111418;
    --surface-subtle: #15181D;
    --surface-hover: #191D23;
    /* Restores the original, already-correct single-layer shadow -
       see the light-mode --shadow-sm comment in :root above for why
       dark mode is deliberately excluded from the strengthened value. */
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);

    --border: #252A31;
    --border-subtle: #1D2127;

    --text-primary: #F4F4F5;
    --text-secondary: #A1A1AA;
    --text-muted: #71717A;
    --text-disabled: #52525B;

    --accent: #3B82F6;
    --accent-hover: #60A5FA;
    --accent-subtle: #172554;
    --accent-border: #1E40AF;
    --accent-rgb: 59, 130, 246;

    --success: #22C55E;
    --success-subtle: rgba(34, 197, 94, 0.14);
    --warning: #F59E0B;
    --warning-subtle: rgba(245, 158, 11, 0.14);
    --danger: #EF4444;
    --danger-subtle: rgba(239, 68, 68, 0.14);
    --info: #60A5FA;
    --info-subtle: rgba(96, 165, 250, 0.14);
}

/* Cross-page navigation cross-fade (2026-09-04, site-wide motion pass,
   punch-list item #7) - the native View Transitions API's opt-in for
   same-origin, full-document navigations (a link click, a search
   form's GET submit, back/forward). No JS, no per-page wiring: when a
   browser supports it (Chromium today), every ordinary navigation
   automatically cross-fades the old page out/new page in instead of
   the usual hard white-flash cut, using the browser's own default
   transition - genuinely a progressive enhancement, an unsupported
   browser (Safari/Firefox as of this writing) just ignores this
   unknown at-rule entirely and navigates exactly as it already did,
   no fallback code needed. Deliberately left at the browser's default
   timing/easing for this first pass rather than hand-tuning
   ::view-transition-old(root)/-new(root) to match --duration/--ease -
   a smaller, safer first step; revisit once this default cross-fade
   has been seen live and judged against the rest of the motion pass. */
@view-transition {
    navigation: auto;
}

/* -------------------------------------------------------------- */
/* Base                                                             */
/* -------------------------------------------------------------- */
body {
    background-color: var(--background);
    color: var(--text-primary);
    font-family: var(--font-ui);
    font-size: 15px;
    /* Sticky footer (2026-09-04, user-reported - the footer sat mid-
       viewport on any page shorter than the screen, e.g. the Whois
       homepage after its own hero was trimmed down). body becomes the
       flex column (nav + main + footer, in source order); main grows
       to fill any leftover space so footer always lands at the true
       bottom, but still flows normally below main on a page tall
       enough to scroll. */
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}
body > main { flex: 1 0 auto; }
/* Wider desktop container (2026-09-03, user-directed) - Bootstrap's
   stock .container caps at 1140px (xl) / 1320px (xxl), which read as
   a narrow centered column on a real desktop viewport for a toolbox
   product. Capped at 1160px rather than left uncapped, so text/card
   rows still stay a comfortable reading width - not simply "wider is
   better". Header/main/footer all use .container already, so this one
   rule keeps all three aligned automatically. */
/* Unconditional, not tiered per Bootstrap breakpoint (2026-09-03,
   user-directed correction - the previous @media(min-width:1200px)
   version left a real gap between 992-1199px, where Bootstrap's own
   .lg container still capped at 960px) - one flat rule, so the
   container is exactly this width from the point it first gets wider
   than the viewport, with no in-between step. Below 1160px+48px of
   padding the browser's own min(100%, max-width) resolves to 100%
   automatically, so this never fights mobile's own fluid width -
   nothing extra needed to "turn it off" below a breakpoint. */
.container {
    width: 100%;
    max-width: 1160px;
    padding-left: 24px;
    padding-right: 24px;
    margin-left: auto;
    margin-right: auto;
}
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
    font-weight: 600;
    letter-spacing: -0.01em;
}
.font-monospace, code, pre {
    font-family: var(--font-mono) !important;
    font-variant-numeric: tabular-nums;
}
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); }
::selection { background: var(--accent-subtle); }

/* Bootstrap 5.3's real .text-muted rule (confirmed against its actual
   shipped CSS, not assumed) is `color: var(--bs-secondary-color)
   !important` - NOT --bs-tertiary-color, which is what every one of
   the 69 `class="text-muted"` usages across 13 view files in this
   codebase actually intended (the class name says "muted"; nothing
   here ever meant "secondary"). Our own bridge maps
   --bs-secondary-color to --text-secondary, so every .text-muted
   element sitewide - Summary card labels foremost among them - has
   been silently rendering a full step darker/stronger than intended,
   flattening exactly the label-vs-value contrast that hierarchy
   depends on. The same "Bootstrap self-scopes this, a :root bridge
   doesn't reach it" trap already hit for card/table backgrounds and
   status badges - just never checked for this specific class until
   the 2026-09-04 light-mode contrast pass (confirmed live via
   getComputedStyle() before writing this fix: Summary's own label
   <th> measured #374151, not the #6B7280 every reasonable person
   reading the class name would expect). Overridden directly with a
   matching !important, the same technique already used for
   .font-monospace above. Deliberately scoped to LIGHT mode only
   (2026-09-04, user-directed - "do not change dark mode") - the
   identical bug exists in dark mode too, but dark mode's current
   rendering is the explicit, approved quality benchmark; the
   [data-theme="dark"] override right below restores its exact
   present-day appearance (equivalent to --text-secondary, unchanged)
   rather than letting this fix silently alter it as a side effect. */
.text-muted { color: var(--text-muted) !important; }
[data-theme="dark"] .text-muted { color: var(--text-secondary) !important; }

/* -------------------------------------------------------------- */
/* Cards - Bootstrap self-scopes --bs-card-bg directly on .card,   */
/* so a :root override alone is a no-op; set the property directly. */
/* -------------------------------------------------------------- */
.card {
    background-color: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    /* 2026-09-13, user-reported "convex/sunken" look at wide viewports -
       a border alone marks an edge but gives no depth cue, so the card's
       flat color-only separation from the page read as receded rather
       than raised. A faint shadow (reusing the existing --shadow-sm
       token, already defined but never consumed until now) lifts the
       card toward the viewer instead. */
    box-shadow: var(--shadow-sm);
}
.card-header {
    background-color: transparent;
    border-bottom-color: var(--border);
    padding: 0.75rem 1.1rem;
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.card-body { padding: 1.1rem; }

/* -------------------------------------------------------------- */
/* Tables - same self-scoping trap as .card                       */
/* -------------------------------------------------------------- */
.table > :not(caption) > * > * {
    background-color: var(--surface);
    border-bottom-color: var(--border-subtle);
    padding: 0.6rem 0.9rem;
    /* Site-wide motion pass, punch-list item #6 (2026-09-04) - row
       hover previously had no transition, a hard cut to --surface-hover. */
    transition: background-color var(--duration) var(--ease);
}
.table > thead > tr > th {
    background-color: var(--surface-subtle);
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
    border-bottom-color: var(--border);
}
.table-hover > tbody > tr:hover > * {
    background-color: var(--surface-hover);
}

/* -------------------------------------------------------------- */
/* Buttons                                                          */
/* -------------------------------------------------------------- */
.btn {
    font-weight: 500;
    font-size: 0.88rem;
    border-radius: var(--radius-sm);
    white-space: nowrap;
    transition: background-color var(--duration) var(--ease),
                border-color var(--duration) var(--ease),
                color var(--duration) var(--ease),
                transform var(--duration) var(--ease);
}
/* Press feedback (2026-09-04, site-wide motion pass, punch-list item
   #5) - existed pre-2026-09-03 design reset, never carried into the
   current system; buttons had zero :active state at all. */
.btn:active { transform: scale(0.97); }
@media (prefers-reduced-motion: reduce) {
    .btn:active { transform: none; }
}
/* A submit button sitting next to a flex-grow input (DNS Lookup's own
   form, and any other tool that follows the same pattern) has
   flex-shrink:1 by default - swapping its label for a longer loading
   string ("Lookup" -> "Looking up...") could shrink the button
   narrower than the new text needs, wrapping it to a second line and
   growing the button's height. flex-shrink:0 keeps its natural width
   instead, white-space above stops the wrap outright either way. */
.btn[type="submit"] {
    flex-shrink: 0;
}
.btn-primary {
    --bs-btn-bg: var(--accent);
    --bs-btn-border-color: var(--accent);
    --bs-btn-hover-bg: var(--accent-hover);
    --bs-btn-hover-border-color: var(--accent-hover);
    --bs-btn-active-bg: var(--accent-hover);
    --bs-btn-active-border-color: var(--accent-hover);
    --bs-btn-focus-shadow-rgb: var(--accent-rgb);
}
.btn-outline-secondary {
    --bs-btn-color: var(--text-secondary);
    --bs-btn-border-color: var(--border);
    --bs-btn-hover-bg: var(--surface-hover);
    --bs-btn-hover-border-color: var(--border);
    --bs-btn-hover-color: var(--text-primary);
}
.btn-ghost {
    background: transparent;
    border: 1px solid transparent;
    color: var(--text-secondary);
}
.btn-ghost:hover { background: var(--surface-hover); color: var(--text-primary); }

/* -------------------------------------------------------------- */
/* Forms                                                            */
/* -------------------------------------------------------------- */
.form-control, .form-select {
    background-color: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-primary);
    font-size: 0.9rem;
    /* 2026-09-13, user-reported follow-up - the "sunken/convex" look
       was traced mainly to .card at first, but the user pointed out it
       shows up most on pages with a large textarea (e.g. Base64
       Encoder & Decoder) - .form-control never had a resting shadow at
       all, only a border, and a big multi-hundred-px textarea is a far
       more dominant flat surface than any card on the page. Same
       --shadow-sm token .card now uses, for the same reason. */
    box-shadow: var(--shadow-sm);
}
.form-control:focus, .form-select:focus {
    background-color: var(--surface);
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.15);
}
.form-control::placeholder { color: var(--text-disabled); }

/* .form-check-input had NO override anywhere in this file (confirmed
   by grep before fixing, 2026-09-09, user-reported: "checkbox
   visibility is poor both in light and dark mode", Base64 Encoder &
   Decoder's own URL-safe checkbox) - pure stock Bootstrap default the
   whole time, which explains it: Bootstrap's own unchecked border is
   a low-opacity black (rgba(0,0,0,.25)), never remapped to this app's
   own --border token the way .form-control's border already is -
   barely visible against a near-black dark-mode surface, and weaker
   than this app's own deliberately-strengthened form-control border
   even in light mode. Matches .form-control's own token usage
   (surface/border/accent), not a new visual language. */
.form-check-input {
    background-color: var(--surface);
    border: 1px solid var(--border);
}
.form-check-input:checked {
    background-color: var(--accent);
    border-color: var(--accent);
}
.form-check-input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.15);
}
.form-check-input:disabled {
    background-color: var(--surface-subtle);
    border-color: var(--border-subtle);
}

/* -------------------------------------------------------------- */
/* Badges                                                           */
/* -------------------------------------------------------------- */
.badge { font-weight: 500; border-radius: var(--radius-sm); }
.badge.status-success { background: var(--success-subtle); color: var(--success); }
.badge.status-warning { background: var(--warning-subtle); color: var(--warning); }
.badge.status-danger  { background: var(--danger-subtle);  color: var(--danger); }
.badge.status-neutral { background: var(--surface-subtle); color: var(--text-muted); border: 1px solid var(--border); }

/* -------------------------------------------------------------- */
/* Alerts                                                           */
/* -------------------------------------------------------------- */
.af-alert {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--text-muted);
    border-radius: var(--radius);
    /* Same card-shadow audit as .card (2026-09-13) - was border-only. */
    box-shadow: var(--shadow-sm);
    padding: 0.75rem 0.9rem;
    color: var(--text-primary);
    font-size: 0.88rem;
}
.af-alert-danger  { border-left-color: var(--danger); }
.af-alert-danger .af-toast-icon { color: var(--danger); }
.af-alert-secondary .af-toast-icon { color: var(--text-muted); }
/* success/warning variants (2026-09-04) - the SSL Checker's summary
   banner needed a "clean/all-good" and "has warnings" state, not just
   the error state this component already had. */
.af-alert-success { border-left-color: var(--success); }
.af-alert-success .af-toast-icon { color: var(--success); }
.af-alert-warning { border-left-color: var(--warning); }
.af-alert-warning .af-toast-icon { color: var(--warning); }

/* -------------------------------------------------------------- */
/* Honeypot field (2026-09-13, contact form) - off-screen via         */
/* absolute positioning, deliberately NOT display:none or             */
/* visibility:hidden (some bots specifically skip filling fields      */
/* hidden that way, precisely to dodge this trick). A sighted or      */
/* screen-reader visitor never encounters it (aria-hidden + no tab    */
/* stop, set on the input itself in the view).                        */
/* -------------------------------------------------------------- */
.af-hp-field { position: absolute; left: -9999px; top: -9999px; }

/* -------------------------------------------------------------- */
/* Status list - vertical pass/fail/warning checklist, each item     */
/* with its own short description (2026-09-04, built for the SSL     */
/* Checker's summary section, but deliberately generic - not         */
/* ssl-prefixed - since any future tool that wants the same          */
/* "quick verdict" list can reuse it as-is). Was a compact multi-     */
/* column grid at first; changed to a single-column vertical list     */
/* (same request) once each item grew a full description line -      */
/* a grid of full sentences reads worse than a scannable stack.      */
/* -------------------------------------------------------------- */
.status-list { display: flex; flex-direction: column; gap: 0.5rem; }
.status-list-item {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    padding: 0.65rem 0.85rem;
    border-radius: var(--radius);
    font-size: 0.85rem;
}
.status-list-item .af-toast-icon { flex: none; margin-top: 0.1rem; }
/* inherit + reduced opacity (not a separate --text-secondary) so the
   description reads as a quieter version of its own item's status
   color, not an unrelated grey sitting on a colored background. */
.status-list-desc { display: block; font-size: 0.78rem; font-weight: 400; color: inherit; opacity: 0.8; }
.status-list-item.status-success { background: var(--success-subtle); color: var(--success); }
.status-list-item.status-warning { background: var(--warning-subtle); color: var(--warning); }
.status-list-item.status-danger  { background: var(--danger-subtle);  color: var(--danger); }
.status-list-item.status-info    { background: var(--surface-subtle); color: var(--text-secondary); }

/* Letter grade badge (2026-09-04, SSL Checker's A+-to-F rating) -
   deliberately generic, not ssl-prefixed, same reasoning as the
   status list above. Reuses the identical 4 color tokens, no new
   literals - a 5th "A+" case doesn't need its own color, it shares
   the same success tint as a plain "A". */
.grade-badge {
    position: relative;
    display: flex; align-items: center; justify-content: center;
    flex: none;
    width: 3.5rem; height: 3.5rem;
    border-radius: var(--radius-lg);
    font-size: 1.4rem; font-weight: 700;
    font-family: var(--font-mono);
}
.grade-badge-success { background: var(--success-subtle); color: var(--success); }
.grade-badge-warning { background: var(--warning-subtle); color: var(--warning); }
.grade-badge-danger  { background: var(--danger-subtle);  color: var(--danger); }
.grade-badge-info    { background: var(--surface-subtle); color: var(--text-secondary); }
/* A+ corner mark (2026-09-04, user-directed) - the badge sits directly
   on the page background here (no wrapping .card), so the punched-out
   ring border is var(--background), not --surface, to actually match
   what's behind it. */
.grade-badge-mark {
    position: absolute; top: -6px; right: -6px;
    width: 20px; height: 20px;
    background: var(--success);
    border: 2px solid var(--background);
    border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
}
.grade-badge-mark svg { width: 11px; height: 11px; display: block; }

/* "How we grade" modal trigger (2026-09-04) - a plain, borderless
   link/icon rather than a full .icon-btn box, since it sits inline
   under the summary banner rather than in a toolbar. */
.grade-info-link {
    display: inline-flex; align-items: center; gap: 0.3rem;
    background: none; border: none; padding: 0; margin: 0;
    font-size: 0.76rem; color: var(--text-muted);
    transition: color var(--duration) var(--ease);
}
.grade-info-link:hover { color: var(--accent); }

/* Certificate Chain (2026-09-04, inspired by - not copied from -
   sslshopper.com's own linked-certificate-cards layout, replacing a
   plain 3-column table) - a vertical run of .card links, each with an
   icon + trust badge on the left and its own compact table-sm of
   details on the right, connected by a small downward chevron. */
.cert-chain { display: flex; flex-direction: column; }
/* Sized up from 16px (2026-09-04, user-reported - read as too small
   against the substantial cards it connects) with a bit of vertical
   padding so it reads as a real connector, not a stray glyph pinched
   between two cards. */
.cert-chain-connector { display: flex; justify-content: center; align-items: center; color: var(--text-disabled); padding: 0.3rem 0; }
.cert-chain-item { margin-bottom: 0.15rem; }
.cert-chain-icon { position: relative; width: 30px; height: 30px; }
.cert-chain-icon-badge {
    position: absolute; right: -4px; bottom: -4px;
    background: var(--surface); border-radius: 50%;
}

/* IP Lookup - raw whois-style record block (2026-09-04, user-directed,
   modeled on a real RIPE whois text response screenshot - Registry
   Details/Contacts/Location consolidated into one card of aligned
   key: value lines, grouped into paragraphs the way a real whois
   record is, instead of three separate tables). A CSS grid per row
   (not fixed-width monospace padding) so the value column wraps
   cleanly on narrow viewports rather than needing horizontal scroll. */
.whois-raw { font-family: var(--font-mono); font-size: 0.85rem; line-height: 1.7; }
.whois-group { margin-bottom: 1.25rem; }
.whois-group:last-child { margin-bottom: 0; }
/* Fixed (not minmax) key-column width, sized to the longest real key
   this card ever prints ("organization:", ~106px at this font-size) -
   a fixed track keeps every row's columns aligned the same way
   minmax's 110px floor used to, but without the leftover 40+px of
   dead space minmax(110px,150px) left after short keys like "type:"/
   "role:" (2026-09-04, user-directed - "reduce the margin ... to make
   it more readable"). Gap also tightened, 0.75rem -> 0.5rem. */
.whois-row { display: grid; grid-template-columns: 118px 1fr; column-gap: 0.5rem; }
/* Key and value deliberately share one color (2026-09-04, user-directed
   - a muted-key/bright-value split, though genuinely identical in
   font-family/weight/size, still reads as "two different fonts" on a
   dark background - the near-white value glows against the
   near-black card in a way flat gray key text doesn't, a
   halation/irradiation effect this project has hit and documented
   before for border colors. Confirmed via an isolated same-color vs.
   different-color test render before concluding this, not guessed.
   User's explicit preference (2026-09-04 follow-up, after an initial
   attempt unified them the other way around): keep .whois-key's own
   muted style as-is, bring .whois-value to match IT, not the reverse. */
.whois-key { color: var(--text-muted); }
.whois-value { color: var(--text-muted); word-break: break-word; }

/* Country flag emoji (country_flag_emoji(), app/helpers.php) sized up
   a bit wherever it needs more visual weight than its default
   text-size rendering - IP Lookup's Summary card "Location" row is
   the first user-directed use of this (2026-09-04). vertical-align
   nudges it back onto the same baseline as the text next to it, since
   bumping font-size alone shifts an inline emoji glyph upward. */
.flag-icon { font-size: 1.5em; line-height: 1; vertical-align: -0.15em; }
.whois-value a { color: var(--accent); }

/* SSL Analyze - per-protocol cipher-suite opener/closer (2026-09-04),
   same opener/closer pattern as the tool-page sidebar's own category
   accordion (layout/tool_sidebar_start.php/assets/js/app.js) - a
   separate instance under its own class names rather than
   generalizing that already-shipped sidebar code, to keep this
   feature's blast radius contained to its own page. */
.cipher-protocol-toggle {
    display: flex; align-items: center; justify-content: space-between; width: 100%;
    background: var(--surface-subtle); border: 1px solid var(--border);
    font: inherit; font-weight: 600; text-align: left; cursor: pointer;
    color: var(--text-primary); padding: 0.6rem 0.85rem;
    border-radius: var(--radius); margin-bottom: 0.5rem;
}
.cipher-protocol-toggle:hover { background: var(--surface-hover); }
.cipher-protocol-toggle-chevron {
    color: var(--text-disabled); flex: none;
    transition: transform var(--duration) var(--ease);
}
.cipher-protocol-toggle[aria-expanded="true"] .cipher-protocol-toggle-chevron { transform: rotate(90deg); }
/* Height animated via inline max-height set by app.js's shared
   afPanelOpen()/afPanelClose() (2026-09-04, site-wide motion pass,
   punch-list item #2) - was an instant `hidden`-attribute toggle with
   nothing to transition. overflow:hidden clips the panel at whatever
   max-height JS is currently animating toward. */
.cipher-protocol-body { overflow: hidden; transition: max-height 200ms var(--ease); }
@media (prefers-reduced-motion: reduce) {
    .cipher-protocol-body { transition: none; }
}

/* -------------------------------------------------------------- */
/* Navbar                                                           */
/* -------------------------------------------------------------- */
.navbar {
    /* Sticky header (2026-09-04, user-directed) - pins the nav to the
       top of the viewport while the page scrolls beneath it. 1030
       matches Bootstrap's own conventional sticky/fixed-navbar z-index
       (--bs-navbar-zindex) - comfortably above ordinary page content
       (cards/tables carry no explicit z-index), comfortably below the
       mega-menu panel (1050)/mobile full-screen nav overlay (1045),
       both of which are DOM descendants of this element anyway and so
       render above its own background regardless, and below the
       top-loading-bar (2000)/command palette (2200), which should
       always show above a sticky nav, not under it. */
    position: sticky;
    top: 0;
    z-index: 1030;
    background-color: var(--surface);
    border-bottom: 1px solid var(--border);
    --bs-navbar-padding-y: 0.7rem;
}
.navbar-brand { font-size: 1.05rem; font-weight: 700; color: var(--text-primary); }
.navbar-brand .brand-accent { color: var(--accent); }
.navbar-nav .nav-link {
    padding-left: 0.75rem;
    padding-right: 0.75rem;
    font-size: 0.88rem;
    font-weight: 500;
    color: var(--text-secondary);
}
.navbar-nav .nav-link:hover { color: var(--text-primary); }
.navbar-toggler {
    width: 2.1rem;
    height: 2.1rem;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: none;
}
.navbar-toggler:focus { box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.15); }
/* Hamburger <-> X (2026-09-03) - Bootstrap's own collapse plugin
   already keeps aria-expanded in sync on this button, so that's the
   only state hook needed; each <line> gets its own transform so the
   top/bottom lines rotate into an X and the middle one fades, instead
   of swapping to a second icon. transform-box: fill-box makes an SVG
   line's own transform-origin its own center rather than the SVG
   viewport's origin - without it every rotation would also orbit
   around (0,0) instead of spinning in place. */
.navbar-toggler-line {
    transform-box: fill-box;
    transform-origin: center;
    transition: transform var(--duration) var(--ease), opacity var(--duration) var(--ease);
}
.navbar-toggler[aria-expanded="true"] .navbar-toggler-line-top { transform: translateY(5px) rotate(45deg); }
.navbar-toggler[aria-expanded="true"] .navbar-toggler-line-mid { opacity: 0; }
.navbar-toggler[aria-expanded="true"] .navbar-toggler-line-bottom { transform: translateY(-5px) rotate(-45deg); }
.dropdown-menu {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    --bs-dropdown-link-hover-bg: var(--surface-hover);
    --bs-dropdown-link-active-bg: var(--accent);
    --bs-dropdown-header-color: var(--text-muted);
}
/* Same entrance animation as .mega-panel below, reusing its keyframe
   (2026-09-04, site-wide motion pass, punch-list item #4) - not
   currently reachable in this Suite's own live markup (the nav's
   "Tools"/"Categories" use the custom .mega-panel system, not
   Bootstrap's own dropdown), kept for whichever future page adopts a
   plain Bootstrap dropdown so it doesn't ship without one. */
.dropdown-menu.show { animation: mega-fade-in var(--duration) var(--ease); }
@media (prefers-reduced-motion: reduce) {
    .dropdown-menu.show { animation: none; }
}

/* Mega menu (2026-09-03) - "Tools"/"Categories" nav items. Same
   surface/border/radius/shadow language as .dropdown-menu above, just
   a custom trigger+panel pair instead of Bootstrap's dropdown
   component (a plain dropdown-menu can't do the "browse, not just a
   flat link list" panel shape this needed - search row, tool cards
   with description, multi-column categories). assets/js/app.js owns
   the open/close/hover-delay/keyboard behavior; this file only ever
   toggles the `hidden` attribute, so the closed state costs nothing
   and needs no JS-vs-CSS state to stay in sync. */
.dropdown-mega { position: relative; }
.dropdown-mega > .nav-link {
    display: flex; align-items: center; gap: 0.3rem;
    background: none; border: none; cursor: pointer;
}
.mega-chevron { color: var(--text-disabled); transition: transform var(--duration) var(--ease); }
.dropdown-mega > .nav-link[aria-expanded="true"] .mega-chevron { transform: rotate(180deg); }

.mega-panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.9rem;
}
@media (min-width: 768px) {
    .mega-panel {
        position: absolute;
        top: calc(100% + 8px);
        left: 0;
        z-index: 1050;
        box-shadow: var(--shadow);
        animation: mega-fade-in var(--duration) var(--ease);
    }
    .mega-panel-tools { width: 300px; }
    .mega-panel-categories { width: min(760px, calc(100vw - 48px)); }
}
@media (max-width: 767.98px) {
    .mega-panel { border: none; box-shadow: none; padding: 0.5rem 0 0.5rem 0.75rem; margin-bottom: 0.25rem; }
}

/* Custom-styled dropdown (views/home/_tools-quick-search.php's own
   tool picker) - NOT a native <select>. A native <select>'s own open
   popup is browser/OS-native chrome: `color-scheme` (see :root's own
   comment near the top of this file) fixed it on some platforms but
   confirmed live NOT all (2026-09-09, user-reported with a screenshot,
   after the color-scheme fix was already deployed) - some browser/OS
   combinations render a <select>'s popup list using native theming
   that page CSS simply cannot reach, `color-scheme` included. This
   sidesteps that entire cross-browser inconsistency by never
   delegating to native popup rendering at all - the toggle button
   reuses .form-select's own look, but the open menu is a plain
   absolutely-positioned <ul> styled with this app's own tokens
   (var(--surface) etc.), the same ones every other themed surface in
   this Suite already uses correctly in both themes - guaranteed
   consistent regardless of OS/browser, the same reasoning
   .mega-panel above already relies on for the same reason. */
.af-dropdown { position: relative; }
.af-dropdown-toggle {
    display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
    cursor: pointer; text-align: left; white-space: nowrap; width: 100%;
    /* This toggle borrows .form-select purely for its BOX styling
       (border/background/radius/font, all re-pointed at this app's own
       tokens above) - it's a <button>, not a real <select>, but
       Bootstrap's own .form-select rule also paints its own chevron
       via background-image (plus reserves right padding for it),
       which doesn't know or care what element it's applied to.
       Confirmed live (2026-09-09, user-reported with a screenshot):
       that left a SECOND chevron sitting right next to this
       component's own explicit .af-dropdown-chevron SVG. Cleared
       here, not worked around in the SVG - one arrow, drawn once. */
    background-image: none;
    padding-right: 0.85rem;
}
.af-dropdown-chevron { flex: none; color: var(--text-muted); transition: transform var(--duration) var(--ease); }
.af-dropdown[data-open="true"] .af-dropdown-chevron { transform: rotate(180deg); }
.af-dropdown-menu {
    position: absolute; top: calc(100% + 4px); left: 0; z-index: 1050;
    min-width: 100%; max-height: 320px; overflow-y: auto;
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: var(--shadow); padding: 0.35rem; margin: 0; list-style: none;
    animation: mega-fade-in var(--duration) var(--ease);
}
.af-dropdown-menu[hidden] { display: none; }
.af-dropdown-menu li {
    padding: 0.45rem 0.6rem; border-radius: var(--radius-sm); font-size: 0.9rem;
    color: var(--text-primary); cursor: pointer; white-space: nowrap;
}
.af-dropdown-menu li:hover,
.af-dropdown-menu li.af-dropdown-active { background: var(--surface-hover); }
.af-dropdown-menu li[aria-selected="true"] { font-weight: 600; color: var(--accent); }

/* Full-screen mobile nav overlay (2026-09-03, user-directed) -
   Bootstrap's own .collapse only ever grows to its own content's
   natural height, pushing the page content below it down instead of
   covering the screen. Repositioned as a fixed, full-viewport panel
   that starts right below the navbar bar itself, only while actually
   open (.show/.collapsing - the collapsed, closed state is untouched,
   still hidden via Bootstrap's own .collapse:not(.show){display:none}).
   --af-navbar-h is set from assets/js/app.js (the navbar's real
   rendered height varies slightly by content/theme, not safe to
   hardcode); assets/js/app.js also locks page scroll behind it via
   the body.af-nav-open class below, toggled on Bootstrap's own
   shown.bs.collapse/hidden.bs.collapse events. */
@media (max-width: 767.98px) {
    .navbar-collapse.show,
    .navbar-collapse.collapsing {
        position: fixed;
        top: var(--af-navbar-h, 57px);
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 1045;
        display: flex;
        flex-direction: column;
        /* Bootstrap's own base .navbar-collapse{} rule (unconditional,
           unrelated to any breakpoint) sets align-items:center - left
           in place everywhere else, but it shrink-wraps/centers this
           panel's direct children (.navbar-nav, .nav-trailing) unless
           overridden, which is what centered the theme-toggle button
           below the nav links. */
        align-items: stretch;
        background: var(--surface);
        padding: 1.25rem 1.5rem 2rem;
        overflow-y: auto;
        /* Bootstrap animates .collapsing by height, which for a
           full-viewport panel reads as a slow curtain drop rather than
           a snappy reveal - skipped in favor of the fade/slide below. */
        transition: none;
        height: auto !important;
        /* .collapsing has to start from the exact same invisible/offset
           state the .show animation below starts from, not fully
           visible - Bootstrap's own JS holds the panel in .collapsing
           only very briefly (it reads this rule's own 0s transition
           duration and skips straight to .show), so without this, the
           panel would flash instantly to fully visible during that
           split-second .collapsing phase, then the .show animation
           below would restart it from invisible - the reported jitter. */
        opacity: 0;
        transform: translateY(-6px);
    }
    .navbar-collapse.show { animation: af-mobile-nav-in 0.16s var(--ease) forwards; }
    .navbar-collapse .navbar-nav { width: 100%; }
    .navbar-collapse .nav-item { width: 100%; }
    .navbar-collapse .nav-link {
        display: block;
        width: 100%;
        text-align: left;
        font-size: 1rem;
        padding: 0.85rem 0.1rem;
        border-bottom: 1px solid var(--border);
    }
    /* The mega-menu triggers are real <button> elements - the browser's
       own UA stylesheet defaults a button's text-align to center, which
       silently overrode the plain .nav-link rule above the moment it
       switched from flex to display:block. Kept flex here instead
       (higher specificity than the block rule above, so it wins), label
       left / chevron right via justify-content. */
    .navbar-collapse .dropdown-mega > .nav-link {
        display: flex;
        width: 100%;
        justify-content: space-between;
    }
}
body.af-nav-open { overflow: hidden; }
@keyframes af-mobile-nav-in {
    from { opacity: 0; transform: translateY(-6px); }
    to { opacity: 1; transform: translateY(0); }
}

/* Mobile nav-trailing stacking (2026-09-03, real regression caught via
   Playwright screenshot, not visible from curl/markup checks alone) -
   this rule existed once already this session, then got dropped when
   app.css was rewritten wholesale for the design-system reset. Without
   it, the theme toggle stays a fixed 2.1rem square (.icon-btn's own
   base size) even though it's also carrying a "Dark mode" text label
   on mobile - the label wraps and clips inside that tiny square
   instead of the button growing to fit it. */
@media (max-width: 767.98px) {
    .nav-trailing {
        flex-direction: column;
        align-items: stretch !important;
        border-top: 1px solid var(--border);
        margin-top: 0.5rem;
        padding-top: 0.75rem;
    }
    .nav-trailing #afThemeToggle {
        width: 100%;
        height: auto;
        justify-content: flex-start;
        padding: 0.55rem 0.75rem;
    }
}

/* iOS Safari zooms the whole page on focus for any text input under
   16px - .form-control/.hero-search/.cmdk-input-row all deliberately
   use a smaller size for desktop density (0.9-0.95rem, 14.4-15.2px),
   which is what was triggering it. Bumped to exactly 16px on mobile
   only, so desktop's chosen type scale is untouched. */
@media (max-width: 767.98px) {
    .form-control,
    .form-select {
        font-size: 16px;
    }
}
/* JSON Formatter's own textarea (#jsonInput, views/json/_tool_page.php) -
   wants a smaller, monospace-appropriate desktop size than
   .form-control's own 0.9rem default, so it can't just rely on the
   general rule above untouched. First reported live (2026-09-09,
   user-directed) as an inline `style="font-size:0.85rem"` on the
   element itself - inline styles beat ANY stylesheet rule regardless
   of specificity or media query, so that inline override was ALSO
   defeating the general iOS Safari fix just above on mobile, the
   exact bug that fix exists to prevent. Fixed the same way
   .hero-search input's own equivalent case already is: a real rule
   here for desktop, a same-selector @media override declared AFTER it
   (same specificity, later wins) for mobile - no inline style left to
   fight the cascade. Kept even after CodeMirror (below) took over the
   VISIBLE editing surface - #jsonInput is still the real underlying
   textarea CodeMirror.fromTextArea() wraps and keeps hidden-but-synced,
   so this rule is harmless dead weight now, not worth removing. */
#jsonInput { font-size: 0.85rem; white-space: pre; }
@media (max-width: 767.98px) {
    #jsonInput { font-size: 16px; }
}

/* CodeMirror 5 (2026-09-09) - the visible editing surface for the JSON
   Formatter now that CodeMirror.fromTextArea() has taken over
   #jsonInput above. `.json-editor` is a class assets/js/json-formatter.js
   adds to CodeMirror's own wrapper element (cm.getWrapperElement()),
   not a class from CodeMirror itself - lets this styling stay scoped
   to this one tool without touching CodeMirror's own base classes,
   ready to be reused by a future formatter/validator tool the same
   way. Box styling matches .form-control's own (border/radius/
   background tokens) so it reads as one consistent input surface with
   the rest of the site, not a visually foreign embedded widget. Fixed
   height + internal scroll (not viewportMargin:Infinity/auto-grow) -
   matches the original plain <textarea>'s own bounded, scrollable
   behavior (rows="16") rather than letting a huge pasted document
   push the whole page down. */
/* `.json-editor` and `.CodeMirror` land on the SAME element -
   cm.getWrapperElement() (what json-formatter.js adds `.json-editor`
   to) IS the library's own `.CodeMirror` div, not an ancestor of it.
   Every override below therefore uses the compound selector
   `.json-editor.CodeMirror` (specificity 0,2,0) rather than a
   descendant combinator like `.json-editor .CodeMirror` (0,2,0 too,
   but matches nothing here - there's no such descendant) or a bare
   `.json-editor` alone (0,1,0, same as CodeMirror's own base
   `.CodeMirror{background:#fff;...}` rule from lib/codemirror.css -
   ties on specificity resolve by source order, and that stylesheet is
   linked AFTER app.css, so a same-specificity override here would
   silently lose regardless of which came first in this file).
   Confirmed live before fixing, not assumed: a bare `.json-editor`
   rule really did lose to CodeMirror's own white background in dark
   mode. */
.json-editor.CodeMirror {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    height: 26rem;
    background: var(--surface);
    color: var(--text-primary);
    font-family: var(--bs-font-monospace, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
    font-size: 0.85rem;
    /* Same card-shadow audit as .form-control/.card (2026-09-13) - this
       is the JSON Formatter's own large, border-only editing surface,
       the same "big flat panel with no depth cue" gap .form-control had. */
    box-shadow: var(--shadow-sm);
}
.json-editor.CodeMirror .CodeMirror-gutters { background: var(--surface-subtle); border-right: 1px solid var(--border); }
.json-editor.CodeMirror .CodeMirror-linenumber { color: var(--text-disabled); }
.json-editor.CodeMirror .CodeMirror-cursor { border-left-color: var(--text-primary); }
.json-editor.CodeMirror .CodeMirror-selected { background: rgba(var(--accent-rgb), 0.18); }
@media (max-width: 767.98px) {
    /* Same iOS Safari zoom-on-focus fix as #jsonInput's own rule above
       (any input under 16px triggers it), applied to CodeMirror's real
       DOM shape - the actual focusable/typed-into element on mobile is
       a plain <textarea> CodeMirror positions off-screen inside its
       own wrapper, not #jsonInput itself (that one's hidden and
       untouched once CodeMirror takes over), so the fix has to target
       IT specifically, not just the outer .json-editor font-size. */
    .json-editor.CodeMirror,
    .json-editor.CodeMirror textarea {
        font-size: 16px;
    }
}

/* Error-row highlighting (2026-09-09, user-directed - "highlight the
   row" for the exact line locateJsonError()/describeError() already
   compute). --danger-subtle is already this app's own theme-aware
   translucent tint (see the badge/status-list-item/grade-badge
   entries above) - reused directly rather than a new one-off color. */
.json-editor .cm-af-error-line { background: var(--danger-subtle); }
.af-error-gutter-marker {
    display: block;
    width: 6px;
    height: 6px;
    margin: 0.55rem auto 0;
    border-radius: 50%;
    background: var(--danger);
}

/* CodeMirror 5's own bundled "default" theme (lib/codemirror.css) is
   tuned for a white page and reads illegibly low-contrast against this
   app's near-black dark-mode surface - same class of bug already fixed
   once for native form-control popups (see the color-scheme/.af-dropdown
   entries above), just for syntax-highlighting colors instead of chrome.
   Overrides only the token classes JSON actually produces (string/
   number/atom/property/punctuation) rather than the mode's full
   language-agnostic set - deliberately scoped to what this tool needs,
   not a general-purpose CodeMirror dark theme. */
[data-theme="dark"] .json-editor .cm-string   { color: #7EE787; }
[data-theme="dark"] .json-editor .cm-number   { color: #79C0FF; }
[data-theme="dark"] .json-editor .cm-atom     { color: #FF7B72; }
[data-theme="dark"] .json-editor .cm-property { color: #D2A8FF; }
[data-theme="dark"] .json-editor .cm-punctuation,
[data-theme="dark"] .json-editor .cm-meta {
    color: var(--text-secondary);
}
@keyframes mega-fade-in {
    from { opacity: 0; transform: translateY(-4px); }
    to { opacity: 1; transform: translateY(0); }
}

.mega-search-row {
    display: flex; align-items: center; gap: 0.55rem; width: 100%;
    padding: 0.55rem 0.7rem; border: none; background: var(--surface-subtle);
    border-radius: var(--radius-sm); color: var(--text-secondary); font-size: 0.85rem;
    margin-bottom: 0.8rem; cursor: pointer;
}
.mega-search-row:hover { background: var(--surface-hover); color: var(--text-primary); }
.mega-panel-label {
    font-size: 0.68rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--text-disabled); margin-bottom: 0.5rem;
}
.mega-tool-link {
    display: flex; align-items: flex-start; gap: 0.6rem;
    padding: 0.45rem 0.5rem; border-radius: var(--radius-sm); text-decoration: none; color: var(--text-primary);
}
.mega-tool-link:hover { background: var(--surface-hover); }
/* Sized/colored to match the homepage cards (2026-09-03, user-directed
   - "same icons sitewide") - per-tool color via tool_icon_color()
   (layout/header.php), same helper the homepage cards use. */
.mega-tool-link .tool-icon { display: flex; align-items: center; justify-content: center; width: 1.9rem; height: 1.9rem; flex: none; }
.mega-tool-link .tool-icon svg { width: 20px; height: 20px; }
.mega-tool-link .mega-tool-title { font-size: 0.85rem; font-weight: 600; }
.mega-tool-link .mega-tool-desc { font-size: 0.74rem; color: var(--text-muted); }

.mega-viewall {
    display: block; margin-top: 0.6rem; padding-top: 0.6rem;
    border-top: 1px solid var(--border-subtle);
    font-size: 0.8rem; color: var(--accent); text-decoration: none;
}
.mega-viewall-small { display: block; margin-top: 0.35rem; font-size: 0.76rem; color: var(--accent); text-decoration: none; }

.mega-columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1.25rem; }
.mega-column-title {
    display: flex; align-items: center; gap: 0.4rem;
    font-size: 0.72rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
    color: var(--text-muted); margin-bottom: 0.5rem;
}
/* Per-category color set inline (category_icon_color(), layout/
   header.php) overrides this - kept as the fallback for any future
   category the color map doesn't cover. */
.mega-column-title svg { color: var(--text-disabled); width: 16px; height: 16px; }
.mega-column-link { display: block; padding: 0.3rem 0; font-size: 0.85rem; color: var(--text-secondary); text-decoration: none; }
.mega-column-link:hover { color: var(--accent); }

/* Icon buttons (theme toggle, ⌘K trigger, copy/refresh actions) */
.icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.1rem;
    height: 2.1rem;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text-secondary);
    transition: background-color var(--duration) var(--ease), color var(--duration) var(--ease), transform var(--duration) var(--ease);
}
.icon-btn:hover { background: var(--surface-hover); color: var(--text-primary); }
/* Press feedback (2026-09-04, site-wide motion pass, punch-list item
   #5) - same treatment as .btn, applied to every other real icon-
   button class on the site so clicking feels consistent everywhere,
   not just on Bootstrap buttons. */
.icon-btn:active, .af-copy-btn:active, .af-refresh-btn:active, .email-reveal-btn:active { transform: scale(0.93); }
@media (prefers-reduced-motion: reduce) {
    .icon-btn:active, .af-copy-btn:active, .af-refresh-btn:active, .email-reveal-btn:active { transform: none; }
}
/* Mac vs. Windows/Linux keyboard-shortcut hint (2026-09-13,
   user-directed) - layout/header.php's own early, before-first-paint
   script sets data-shortcut-mod on <html> ("mac" or "ctrl"); these two
   spans (markup: <span class="kbd-mod-mac">⌘</span><span
   class="kbd-mod-ctrl">Ctrl</span>K inside a .kbd-hint) swap on that
   attribute, same toggle-by-attribute pattern the sun/moon/monitor
   theme icons already use. No JS-disabled flash risk either way -
   default (no attribute) shows "Ctrl", the more common case. */
.kbd-hint .kbd-mod-mac { display: none; }
.kbd-hint .kbd-mod-ctrl { display: inline; }
:root[data-shortcut-mod="mac"] .kbd-hint .kbd-mod-mac { display: inline; }
:root[data-shortcut-mod="mac"] .kbd-hint .kbd-mod-ctrl { display: none; }
.kbd-hint {
    display: inline-flex;
    align-items: center;
    gap: 0.15rem;
    font-family: var(--font-mono);
    font-size: 0.7rem;
    color: var(--text-muted);
    background: var(--surface-subtle);
    border: 1px solid var(--border);
    border-radius: 5px;
    padding: 0.1rem 0.35rem;
}

/* Theme toggle icon swap */
/* Theme toggle icon (2026-09-03) - keyed off data-theme-preference
   (what the user actually chose: system/light/dark), NOT data-theme
   (the resolved light/dark render) - a 'system' preference that
   happens to resolve to dark should still show the monitor icon, not
   the moon, so the icon always reflects the user's real setting. */
.af-icon-sun, .af-icon-moon, .af-icon-system { display: none; }
[data-theme-preference="light"] .af-icon-sun { display: inline; }
[data-theme-preference="dark"] .af-icon-moon { display: inline; }
[data-theme-preference="system"] .af-icon-system { display: inline; }

/* -------------------------------------------------------------- */
/* Footer                                                           */
/* -------------------------------------------------------------- */
.site-footer {
    background: var(--surface);
    border-top: 1px solid var(--border);
}
.footer-links a { color: var(--text-secondary); text-decoration: none; font-size: 0.85rem; }
.footer-links a:hover { color: var(--accent); }
.footer-divider { border-top: 1px solid var(--border); }

/* -------------------------------------------------------------- */
/* Tool / category cards (homepage)                                 */
/* -------------------------------------------------------------- */
.tool-card {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.95rem 1.05rem;
    height: 100%;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    /* Same card-shadow audit as .card (2026-09-13) - was border-only
       like every other card-type component this pass fixed. */
    box-shadow: var(--shadow-sm);
    color: var(--text-primary);
    text-decoration: none;
    transition: border-color var(--duration) var(--ease), background-color var(--duration) var(--ease);
}
.tool-card:hover { border-color: var(--accent-border); background: var(--surface-hover); color: var(--text-primary); }
/* Sized + colored to match Mockup.png (2026-09-03, user-reported - the
   icons here read as small, and the mockup's own icons are plain
   colored glyphs at roughly double this size, not a small tinted
   badge box) - the tinted background/box from the earlier version is
   gone entirely; color now comes per-tool from tool_icon_color()
   (views/home/index.php), not a single flat --accent here. */
.tool-card .tool-icon {
    display: flex; align-items: center; justify-content: center;
    width: 2rem; height: 2rem; flex: none;
    margin-top: 0.1rem;
}
.tool-card .tool-icon svg { width: 28px; height: 28px; }
.tool-card .tool-title { font-size: 0.92rem; font-weight: 600; margin-bottom: 0.2rem; }
.tool-card .tool-desc { font-size: 0.82rem; color: var(--text-secondary); line-height: 1.5; }
.tool-card .tool-arrow { margin-left: auto; margin-top: 0.15rem; color: var(--text-disabled); flex: none; }

.category-card {
    display: flex; align-items: center; gap: 0.75rem;
    padding: 0.95rem 1.05rem;
    height: 100%;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    /* Same card-shadow audit as .card/.tool-card (2026-09-13). */
    box-shadow: var(--shadow-sm);
    color: var(--text-primary); text-decoration: none;
    transition: border-color var(--duration) var(--ease);
}
.category-card:hover { border-color: var(--accent-border); color: var(--text-primary); }
/* Same Mockup.png-matched treatment as .tool-card .tool-icon above -
   plain colored glyph, no tinted background box, color set per-
   category by category_icon_color() (views/home/index.php). */
.category-card .cat-icon {
    display: flex; align-items: center; justify-content: center;
    width: 2rem; height: 2rem; flex: none;
}
.category-card .cat-icon svg { width: 28px; height: 28px; }
.category-card .cat-title { font-size: 0.92rem; font-weight: 600; }
.category-card .cat-count { font-size: 0.8rem; color: var(--text-muted); }
.category-card .cat-arrow { margin-left: auto; color: var(--text-disabled); }

/* -------------------------------------------------------------- */
/* Hero search                                                      */
/* -------------------------------------------------------------- */
.hero-search {
    display: flex; align-items: center; gap: 0.6rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.55rem 0.85rem;
    width: 100%; max-width: 440px; height: 42px; margin: 0 auto;
    /* Same card-shadow audit as .form-control (2026-09-13) - this is
       structurally the same "search input panel" gap, just on the
       homepage instead of a tool page. Overridden on focus below,
       same as .form-control's own accent focus ring. */
    box-shadow: var(--shadow-sm);
    transition: border-color var(--duration) var(--ease), box-shadow var(--duration) var(--ease);
}
.hero-search:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.15); }
.hero-search input {
    border: none; background: transparent; flex: 1; font-size: 0.92rem; color: var(--text-primary); outline: none;
}
.hero-search input::placeholder { color: var(--text-disabled); }
.hero-search .search-icon { color: var(--text-muted); flex: none; }
/* iOS Safari zoom-on-focus fix (see the shared media-query block near
   .nav-trailing above) - kept here, after the base .hero-search input
   rule, so it actually wins the cascade on mobile instead of being
   overridden by that later, unconditional rule. */
@media (max-width: 767.98px) {
    .hero-search input { font-size: 16px; }
}

/* -------------------------------------------------------------- */
/* Breadcrumb                                                       */
/* -------------------------------------------------------------- */
/* margin-bottom 1.5rem -> 2rem (2026-09-09, user-reported across
   every tool page, screenshotted on JSON Formatter: "title tight to
   breadcrumb"). Measured live before changing it (Playwright
   getBoundingClientRect): the real gap was already exactly 24px
   (1.5rem), not a rendering bug - just genuinely too tight against
   this shell's own H1 size. One shared rule, since every tool page
   uses this same breadcrumb via layout/tool_sidebar_start.php - fixes
   all of them from here, no per-tool edit needed. */
.af-breadcrumb {
    display: flex; align-items: center; gap: 0.4rem;
    font-size: 0.82rem; color: var(--text-muted); margin-bottom: 2rem;
}
.af-breadcrumb a { color: var(--text-muted); }
.af-breadcrumb a:hover { color: var(--accent); }
.af-breadcrumb .sep { color: var(--text-disabled); }
.af-breadcrumb .current { color: var(--text-primary); }

/* -------------------------------------------------------------- */
/* Sidebar (tool pages)                                             */
/* -------------------------------------------------------------- */
/* Fixed sidebar width, not a Bootstrap col-lg-* fraction (2026-09-04,
   user-reported against Mockup.png: even col-lg-3, ~25% and up from
   the original col-lg-2, still read as "needs a bit more" - and
   Bootstrap's own 12-column grid has no step between col-lg-3 (25%)
   and col-lg-4 (33%) to land on the mockup's own measured ratio
   in between). 300px is close to what the mockup's own sidebar would
   occupy if its removed third (right-rail) column's space were
   redistributed between just these two - measured from the mockup
   image directly, not guessed. .col-12 (still on both columns, in
   layout/tool_sidebar_start.php) keeps the existing full-width mobile
   stack; this only takes over at the same lg breakpoint the sidebar
   itself already appears at (d-none d-lg-block). */
.tool-sidebar-col { flex: 0 0 auto; }
.tool-main-col { flex: 1 1 auto; min-width: 0; }
@media (min-width: 992px) {
    .tool-sidebar-col { flex: 0 0 300px; max-width: 300px; }
    .tool-main-col { flex: 1 1 0%; max-width: none; }
}
/* Matches Mockup.png (2026-09-03, user-reported - the sidebar had no
   visual separation from the main content column at all): a plain
   right-hand divider line plus a faint shadow bleeding off it, the
   same subtle depth cue the mockup uses instead of a fully-boxed
   card. */
.tool-sidebar {
    /* Sticky sidebar (2026-09-04, user-directed, follows the sticky
       navbar above) - docks just below the navbar and scrolls with the
       page up to that point, rather than scrolling away entirely on a
       tall result page and leaving blank space beside the content.
       --af-navbar-h is the same real-measured-height token
       assets/js/app.js already computes for the mobile full-screen nav
       overlay (the navbar's rendered height varies slightly by content/
       theme, not safe to hardcode) - reused here rather than guessing a
       second, possibly-wrong pixel value. max-height + overflow-y:auto
       so a future, longer category list scrolls internally instead of
       running off the bottom of the viewport; only ever visible at the
       lg breakpoint and up in the first place (this column is d-none
       below that), so no separate mobile guard is needed. */
    position: sticky;
    top: calc(var(--af-navbar-h, 57px) + 1rem);
    max-height: calc(100vh - var(--af-navbar-h, 57px) - 2rem);
    overflow-y: auto;
    font-size: 0.86rem;
    padding-right: 1.25rem;
    border-right: 1px solid var(--border);
    box-shadow: 3px 0 10px -8px rgba(0, 0, 0, 0.18);
}
/* Main content column - right/bottom border removed (2026-09-04,
   user-reported: it visually interfered with buttons and other
   elements sitting near those edges). The sidebar keeps its own
   right-hand border/shadow (.tool-sidebar above) - only this framing
   border around the main content is gone. */
.tool-main {
    padding-bottom: 1.5rem;
    /* Settle-in on page load (2026-09-04, site-wide motion pass,
       punch-list item #3) - reuses .af-content-fade-in's own keyframe
       (defined further down, valid regardless of declaration order in
       CSS) rather than a plain class, since every real navigation on
       this multi-page site is a fresh document load - a CSS animation
       on an always-present element already plays automatically the
       moment it's painted, no JS trigger needed here the way item #1's
       dynamic skeleton->content swap required. Every tool page uses
       this one shared column (layout/tool_sidebar_start.php/_end.php),
       so this covers all of them from one rule. */
    animation: af-content-fade-in 200ms var(--ease);
}
@media (prefers-reduced-motion: reduce) {
    .tool-main { animation: none; }
}
/* --text-muted, not --text-disabled (2026-09-04, user-directed light-
   mode readability pass, scoped to sidebar/category navigation) -
   confirmed via screenshot that "CATEGORIES" read distinctly fainter
   than every other sidebar line (Domain/Network/DNS Lookup, all
   --text-secondary) once those got the earlier contrast bump; this is
   a section label a visitor is meant to read, not a disabled control,
   so it belongs at the "metadata" tier the brief calls out, not
   disabled-level. */
.tool-sidebar .sidebar-label {
    font-size: 0.7rem; font-weight: 600; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--text-muted); margin-bottom: 0.5rem;
}
/* Category header is a real <button> now (2026-09-04, matched against
   Mockup.png - each category opens/closes its own tool list, chevron
   rotating to show state) - reset to look identical to the plain <div>
   it replaced, plus the chevron pushed to the far right via
   space-between (icon+label grouped in their own .sidebar-category-label
   span so the 3rd child - the chevron - doesn't get caught in that
   group's own gap). */
.tool-sidebar .sidebar-category {
    display: flex; align-items: center; justify-content: space-between; width: 100%;
    background: none; border: none; font: inherit; text-align: left; cursor: pointer;
    color: var(--text-secondary); font-weight: 600;
    padding: 0.5rem 0.5rem;
    border-radius: var(--radius-sm);
}
.tool-sidebar .sidebar-category:hover { background: var(--surface-hover); }
/* Breathing room between category groups (2026-09-04, user-directed -
   with the right rail gone, the freed-up space goes toward taller,
   less cramped sidebar rows) - only between groups, not above the
   first one, so "Categories" stays close to its own first group. */
.tool-sidebar .sidebar-category:not(:first-child) { margin-top: 1rem; }
.tool-sidebar .sidebar-category-label { display: flex; align-items: center; gap: 0.45rem; }
/* Per-category color set inline (category_icon_color(), layout/
   tool_sidebar_start.php) overrides this fallback - same "same icons
   sitewide" pass as the mega-menu/command-palette (2026-09-03). */
.tool-sidebar .sidebar-category-label svg { color: var(--text-muted); flex: none; width: 16px; height: 16px; }
.tool-sidebar .sidebar-category-chevron {
    color: var(--text-disabled); flex: none;
    transition: transform var(--duration) var(--ease);
}
/* Right-pointing at rest (collapsed); rotated to point down once the
   category's own aria-expanded state (assets/js/app.js) flips true -
   matches the mockup's collapsed-vs-expanded chevron direction exactly. */
.tool-sidebar .sidebar-category[aria-expanded="true"] .sidebar-category-chevron { transform: rotate(90deg); }
/* Left padding indented past where the category's own LABEL text
   starts, not just flush with it (2026-09-04, user-reported against
   Mockup.png pixel-by-pixel: "DNS Lookup" under "Network" starts
   roughly under the "e", not the "N" - a first pass that aligned
   flush with the label's own start still read as under-indented).
   1.95rem is where the label text itself starts (0.5rem button
   padding + 16px icon + 0.45rem gap, confirmed via live
   getBoundingClientRect()); +0.55rem lands roughly one character past
   that, matching the mockup's own visual offset. */
.tool-sidebar .sidebar-link {
    display: block; padding: 0.5rem 0.5rem 0.5rem 2.5rem;
    border-radius: var(--radius-sm); color: var(--text-secondary); text-decoration: none;
}
.tool-sidebar .sidebar-link:hover { background: var(--surface-hover); color: var(--text-primary); }
.tool-sidebar .sidebar-link.is-active { background: var(--accent-subtle); color: var(--accent); font-weight: 500; }
/* Height animated via inline max-height set by app.js's shared
   afPanelOpen()/afPanelClose() (2026-09-04, site-wide motion pass,
   punch-list item #2) - was an instant `hidden`-attribute toggle with
   nothing to transition. */
.tool-sidebar .sidebar-tools { overflow: hidden; transition: max-height 200ms var(--ease); }
@media (prefers-reduced-motion: reduce) {
    .tool-sidebar .sidebar-tools { transition: none; }
}

/* -------------------------------------------------------------- */
/* Right rail (About / Shortcuts / Export)                          */
/* -------------------------------------------------------------- */
/* Same card-shadow audit as .card (2026-09-13) - was border-only. */
.rail-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-sm); padding: 0.9rem; margin-bottom: 0.9rem; }
.rail-card .rail-title { font-size: 0.78rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); margin-bottom: 0.5rem; }
.rail-card p { font-size: 0.82rem; color: var(--text-secondary); margin: 0; line-height: 1.6; }
.rail-shortcut { display: flex; align-items: center; justify-content: space-between; font-size: 0.82rem; color: var(--text-secondary); padding: 0.3rem 0; }
.rail-action {
    display: flex; align-items: center; gap: 0.5rem; width: 100%;
    background: transparent; border: 1px solid var(--border); border-radius: var(--radius-sm);
    padding: 0.45rem 0.65rem; font-size: 0.82rem; color: var(--text-secondary); margin-bottom: 0.4rem;
    transition: background-color var(--duration) var(--ease);
}
.rail-action:hover { background: var(--surface-hover); color: var(--text-primary); }
.rail-action:last-child { margin-bottom: 0; }

/* -------------------------------------------------------------- */
/* Filter tabs                                                      */
/* -------------------------------------------------------------- */
.filter-tabs { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
/* Separates "Raw" (a view-mode toggle) from the actual record-type
   filters it sits alongside (2026-09-03, user-reported: visually read
   as just another type). */
.filter-divider { width: 1px; align-self: stretch; background: var(--border); margin: 0 0.15rem; }
/* Small inline label for an icon-only button group (2026-09-03,
   user-reported: the Export row's 3 icons had nothing but a hover
   tooltip explaining them). Same small-uppercase-muted language as
   .mega-panel-label/.rail-title, just inline instead of block. */
.toolbar-label {
    font-size: 0.68rem; font-weight: 700; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--text-disabled);
}
.filter-tab {
    font-family: var(--font-mono); font-size: 0.78rem; font-weight: 500;
    padding: 0.3rem 0.65rem; border-radius: var(--radius-sm);
    border: 1px solid var(--border); background: var(--surface); color: var(--text-secondary);
    transition: background-color var(--duration) var(--ease), color var(--duration) var(--ease), border-color var(--duration) var(--ease);
}
.filter-tab:hover { background: var(--surface-hover); }
.filter-tab.is-active { background: var(--accent); border-color: var(--accent); color: #fff; }
/* Radio-backed variant (2026-09-09, DNS Propagation Checker's own
   record-type selector) - a real <input type="radio"> (visually
   hidden) immediately followed by a <label class="filter-tab">, so
   selection is native browser/form behavior with zero JS, unlike the
   JS-toggled .is-active class DNS Lookup's own filter tabs use for a
   client-side-only view filter. Cursor added since a <label> has no
   button-like default. */
.filter-tab-radio { position: absolute; opacity: 0; width: 1px; height: 1px; overflow: hidden; }
.filter-tab-radio + .filter-tab { cursor: pointer; }
.filter-tab-radio:checked + .filter-tab { background: var(--accent); border-color: var(--accent); color: #fff; }
.filter-tab-radio:focus-visible + .filter-tab { outline: 2px solid var(--accent); outline-offset: 2px; }

/* -------------------------------------------------------------- */
/* Command palette                                                  */
/* -------------------------------------------------------------- */
.cmdk-backdrop {
    position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4);
    z-index: 2200; display: flex; align-items: flex-start; justify-content: center;
    padding-top: 12vh;
}
.cmdk-panel {
    width: min(560px, 92vw);
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
    box-shadow: var(--shadow); overflow: hidden;
}
.cmdk-input-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.85rem 1rem; border-bottom: 1px solid var(--border); }
.cmdk-input-row svg { color: var(--text-muted); flex: none; }
.cmdk-input-row input { border: none; outline: none; background: transparent; flex: 1; font-size: 0.95rem; color: var(--text-primary); }
/* iOS Safari zoom-on-focus fix (see the .hero-search input rule
   above for the same pattern) - kept after the base rule so it wins
   the cascade on mobile. */
@media (max-width: 767.98px) {
    .cmdk-input-row input { font-size: 16px; }
}
.cmdk-results { max-height: 50vh; overflow-y: auto; padding: 0.4rem; }
.cmdk-item {
    display: flex; align-items: center; gap: 0.6rem;
    padding: 0.55rem 0.6rem; border-radius: var(--radius-sm); cursor: pointer;
    color: var(--text-primary); text-decoration: none;
}
/* Per-tool color set inline (assets/js/app.js, from the icon/color
   fields now embedded in #afCmdkData) overrides this fallback - same
   "same icons sitewide" pass as the mega-menu/sidebar (2026-09-03). */
.cmdk-item .cmdk-icon { color: var(--text-muted); flex: none; width: 18px; height: 18px; }
.cmdk-item .cmdk-desc { font-size: 0.78rem; color: var(--text-muted); }
/* Hover state added (2026-09-03, user-reported - the keyboard-active
   row already had a background via .is-active, but mousing over any
   OTHER row did nothing, unlike .mega-tool-link:hover right above).
   Same background token, same "highlight whichever row is currently
   the interaction target" behavior as the mega menu. */
.cmdk-item:hover { background: var(--surface-hover); }
.cmdk-item.is-active { background: var(--surface-hover); }
.cmdk-empty { padding: 1.5rem; text-align: center; color: var(--text-muted); font-size: 0.85rem; }

/* -------------------------------------------------------------- */
/* Reference list (record-type explainers)                          */
/* -------------------------------------------------------------- */
/* margin-left (2026-09-03, user-reported): these headings sit flush
   at the exact same x as the rounded card/code-block below them
   (Reference's card, Raw's .af-code-block, both border-radius:8-10px)
   - a small, uniform nudge right so the heading doesn't read as
   hanging slightly left of where the rounded corner visually starts.
   Applied to every heading using this class, not just the ones
   directly above a rounded container, so they all stay aligned with
   each other down the page. */
.af-ref-heading { font-size: 0.85rem; font-weight: 600; color: var(--text-primary); margin-top: 1.5rem; margin-bottom: 0.6rem; margin-left: 0.25rem; }
/* Flat glossary layout (2026-09-03) - replaced a grouped bullet list
   (category headings + one long wrapped line per entry) with plain
   label/paragraph pairs, compact and easy to scan without the visual
   noise of bullets or a jagged wrapped-label column. */
.af-ref-dl { margin: 0; }
.af-ref-dl dt {
    font-size: 0.86rem; font-weight: 600; color: var(--text-primary);
    padding-top: 0.9rem; margin-top: 0.9rem; border-top: 1px solid var(--border);
}
.af-ref-dl dt:first-child { padding-top: 0; margin-top: 1.1rem; border-top: none; }
.af-ref-dl dd {
    margin: 0.3rem 0 0; color: var(--text-secondary); font-size: 0.88rem; line-height: 1.6;
}

/* Click-to-reveal email button (2026-09-04, app/helpers.php's
   email_reveal_html()) - reads as a plain text link, not a boxed
   button, since it's replaced by a real <a> the moment it's clicked. */
.email-reveal-btn {
    background: none; border: none; padding: 0; margin: 0; font: inherit;
    color: var(--accent); text-decoration: underline; cursor: pointer;
    display: inline-block;
    transition: color var(--duration) var(--ease), transform var(--duration) var(--ease);
}
.email-reveal-btn:hover { color: var(--accent-hover); }

/* -------------------------------------------------------------- */
/* Loading bar, toast, skeleton, spinners - behavior unchanged,     */
/* restyled to the new tokens.                                      */
/* -------------------------------------------------------------- */
#afLoadingBar {
    position: fixed; top: 0; left: 0; height: 2px; width: 0;
    background: var(--accent); z-index: 2000; opacity: 0; pointer-events: none;
    transition: width 0.4s var(--ease), opacity 0.15s var(--ease);
}
#afLoadingBar.is-active { opacity: 1; width: 75%; }

.af-toast {
    background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--text-muted);
    border-radius: var(--radius); box-shadow: var(--shadow); color: var(--text-primary);
    min-width: 300px; max-width: min(400px, calc(100vw - 2rem));
}
.af-toast-success { border-left-color: var(--success); } .af-toast-success .af-toast-icon { color: var(--success); }
.af-toast-danger  { border-left-color: var(--danger); }  .af-toast-danger  .af-toast-icon { color: var(--danger); }
.af-toast-warning { border-left-color: var(--warning); } .af-toast-warning .af-toast-icon { color: var(--warning); }

.af-code-block {
    background: var(--surface-subtle); color: var(--text-primary);
    border: 1px solid var(--border); border-radius: var(--radius);
    padding: 0.85rem 1rem; font-family: var(--font-mono); font-size: 0.85rem;
    white-space: pre-wrap; word-break: break-word;
}

.af-skeleton { display: block; background: var(--text-disabled); opacity: 0.15; border-radius: var(--radius-sm); animation: af-skeleton-pulse 1.4s var(--ease) infinite; }
.af-skeleton-line { height: 0.85rem; margin-bottom: 0.6rem; }
.af-skeleton-line:last-child { margin-bottom: 0; }
.af-skeleton-badge { display: inline-block; width: 5rem; height: 1.3rem; }
@keyframes af-skeleton-pulse { 0%, 100% { opacity: 0.15; } 50% { opacity: 0.28; } }

/* Generic "content just appeared" settle-in (2026-09-04, site-wide
   motion pass, punch-list item #1) - for a skeleton->real-result swap
   (Whois's whois-detail.js, SSL Analyzer's ssl-analyzer-detail.js),
   which previously just snapped into place via a raw innerHTML/
   outerHTML replace with zero transition, the single most jarring
   moment on the site. Same opacity+translateY shape as the existing
   .mega-panel's own mega-fade-in (a settle-DOWN-into-place motion, the
   right direction for content appearing in the page flow - mega-fade-in
   itself settles from above since it's a dropdown, a different context).
   A dedicated, slightly longer duration than --duration (140ms, tuned
   for snappy hover/press feedback, not a content-appearance moment) -
   200ms read as the right weight without feeling slow. */
.af-content-fade-in { animation: af-content-fade-in 200ms var(--ease); }
@keyframes af-content-fade-in {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
    .af-content-fade-in { animation: none; }
}

.af-submit-spinner, .whois-submit-spinner {
    display: inline-block; width: 0.85rem; height: 0.85rem;
    border: 0.15rem solid rgba(255, 255, 255, 0.5); border-top-color: #fff;
    border-radius: 50%; animation: af-spin 0.6s linear infinite; vertical-align: -0.15em;
}
@keyframes af-spin { to { transform: rotate(360deg); } }

.af-copy-btn, .af-refresh-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.75rem; height: 1.75rem; padding: 0; border: none; border-radius: var(--radius-sm);
    background: transparent; color: var(--text-muted);
    transition: color var(--duration) var(--ease), background-color var(--duration) var(--ease), transform var(--duration) var(--ease);
}
.af-copy-btn:hover, .af-refresh-btn:hover { color: var(--accent); background: var(--surface-hover); }
.af-copy-icon-check { display: none; color: var(--success); }
/* Keyed off the state class alone, not also requiring the .af-copy-btn
   sizing class - a copy button can be styled either way (the compact
   borderless .af-copy-btn, or a plain bordered .icon-btn sitting in a
   toolbar row) and still gets the icon-swap on click. */
.af-copy-btn-done .af-copy-icon-copy { display: none; }
.af-copy-btn-done .af-copy-icon-check { display: inline; }
.af-refresh-btn svg { transition: transform var(--duration) var(--ease); }
.af-refresh-btn:active svg { transform: rotate(180deg); }
/* Disabled for the duration of a scan AND for the 30s post-scan
   cooldown before another manual refresh is allowed (SSL Analyzer,
   assets/js/ssl-analyzer-detail.js) - both states are plain
   greyed-out/unclickable via :disabled alone. The spin animation is
   scoped to the separate .is-scanning class instead (2026-09-04,
   user-reported: applying it to any :disabled state made the button
   keep spinning through the cooldown, well after the scan had already
   finished, reading as "still refreshing" when it wasn't) - JS adds
   .is-scanning only while a scan is genuinely in flight, and strips it
   the moment that scan ends (success or failure), before the cooldown-
   only disable ever takes over. */
.af-refresh-btn:disabled { opacity: 0.5; cursor: default; }
.af-refresh-btn.is-scanning svg { animation: af-spin 1s linear infinite; }
.af-visit-site { display: inline-flex; color: var(--text-muted); transition: color var(--duration) var(--ease); }
.af-visit-site:hover { color: var(--accent); }

/* Real-time scan progress bar (2026-09-04, SSL Analyzer's cipher-suite
   sweep) - a plain 2-part bar (track + fill), not Bootstrap's own
   .progress component, to stay on this app's own token set. */
.af-progress-wrap { margin-bottom: 0; }
.af-progress {
    height: 6px; border-radius: var(--radius-sm); background: var(--surface-subtle);
    border: 1px solid var(--border); overflow: hidden; margin-bottom: 0.6rem;
}
.af-progress-bar { height: 100%; background: var(--accent); border-radius: var(--radius-sm); transition: width 0.4s var(--ease); }
.af-progress-status { font-size: 0.85rem; color: var(--text-muted); }

/* "Cipher scan: <date> [refresh]" row at the top of the SSL Analyzer
   result page - reuses .af-refresh-btn's existing icon-button styling
   above, just needs its own row layout. */
.af-scan-meta { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; color: var(--text-muted); }

#whoisInput.is-invalid { border-color: var(--danger); }
#whoisInput.is-invalid:focus { box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15); }

/* Focus-visible ring (2026-09-03, accessibility polish pass) - the
   custom components in this file (icon buttons, tool/category cards,
   filter tabs, sidebar links, nav links) had no explicit focus state
   of their own; keyboard-only browsers fell back to whatever default
   the browser happens to render, inconsistent with the accent-colored
   ring .form-control/.hero-search already use. One consistent ring,
   same accent color, only shown for keyboard focus (:focus-visible,
   not :focus - a mouse click never triggers this, so it doesn't add
   a visual click-flash anywhere it wasn't already there). */
.icon-btn:focus-visible,
.tool-card:focus-visible,
.category-card:focus-visible,
.filter-tab:focus-visible,
.sidebar-link:focus-visible,
.navbar-brand:focus-visible,
.navbar-nav .nav-link:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* Port Checker: bundle-selection checkboxes (2026-09-08, user-directed).
   A responsive grid of labeled checkbox cards, one per curated bundle -
   each shows the bundle name plus its own port list so a visitor can
   see exactly what a bundle covers without opening a reference card. */
.port-bundle-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 0.5rem;
}
.port-bundle-option {
    display: flex;
    align-items: flex-start;
    gap: 0.55rem;
    padding: 0.6rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: background-color var(--duration) var(--ease), border-color var(--duration) var(--ease);
}
.port-bundle-option:hover { background: var(--surface-hover); }
.port-bundle-option input[type="checkbox"] { margin-top: 0.2rem; cursor: pointer; flex-shrink: 0; }
.port-bundle-name {
    display: block;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--text-primary);
}
.port-bundle-ports {
    display: block;
    font-size: 0.76rem;
    color: var(--text-muted);
    font-family: var(--font-mono);
}
@media (prefers-reduced-motion: reduce) {
    .port-bundle-option { transition: none; }
}

/* -------------------------------------------------------------- */
/* Dropzone (file upload)                                           */
/* -------------------------------------------------------------- */
/* Base64 Encoder & Decoder's own image upload (2026-09-09) - the
   Suite's first drag-and-drop/file-upload UI, styled with existing
   tokens (border/surface-hover/accent), not a new visual language.
   The real <input type="file"> is visually-hidden (see the view's own
   markup) - the dashed box IS its <label>, so both click-to-browse
   (native label-for-input behavior, no JS needed for that part) and
   drag-and-drop (assets/js/base64-formatter.js's own listeners) work
   through the same visible element. */
.af-dropzone {
    border: 1px dashed var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    transition: background-color var(--duration) var(--ease), border-color var(--duration) var(--ease);
}
.af-dropzone-label {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.55rem;
    width: 100%;
    padding: 0.9rem 1rem;
    margin: 0;
    color: var(--text-secondary);
    font-size: 0.85rem;
    cursor: pointer;
}
.af-dropzone-label svg { color: var(--text-muted); flex-shrink: 0; }
.af-dropzone:hover,
.af-dropzone:focus-within {
    background: var(--surface-hover);
}
/* .af-dropzone-active is toggled by JS on dragenter/dragleave/drop -
   a real dragover state, not just :hover, since the drag source is a
   file from outside the browser window, not a mouse hovering the
   element normally. */
.af-dropzone.af-dropzone-active {
    border-color: var(--accent);
    border-style: solid;
    background: rgba(var(--accent-rgb), 0.06);
}
@media (prefers-reduced-motion: reduce) {
    .af-dropzone { transition: none; }
}
