@import "tailwindcss";

/* Scan Blade root + Vue components for Tailwind class usage. */
@source "../views/**/*.blade.php";
@source "../js/**/*.vue";

/*
|--------------------------------------------------------------------------
| Starcity Global design tokens
|--------------------------------------------------------------------------
| Brand = Starcity BLUE (2026-09-04). Navy = near-black charcoal for dark
| surfaces (sidebars, brand panels). The accent scale carries the brand: every
| text-brand-600 link, bg-brand-600 button, active tab, checkbox tick and focus
| ring reads as Starcity.
|
| WHY BLUE, NOT THE LOGO RED (founder, 2026-09-04, re-applied 2026-09-07): the ramp
| was the Starcity red sampled from the logo for one day (#ED1B24 at 500, #D6121B at
| 600 — historical values, deliberately NOT in the ramp below, so the next sweep
| cannot pick them up again). Red is the one hue the whole portal already uses to mean STOP — destructive buttons,
| validation errors, "above market", "negative cashflow" — so a red primary
| button and a red Delete read the same, and a red active tab looked like a
| warning. The logo keeps its red (public/main/images/logo/*.png); the UI moved
| to a blue that sits beside the charcoal and reads as trust. Stock Tailwind
| `blue-*` utilities were used NOWHERE in the codebase, so the brand can own
| this hue without colliding with an informational blue.
|
| The stops are the stock Tailwind blue ramp, chosen for its proven contrast:
|   500 = #3b82f6 — focus rings, marks, chart brand series, accents on light.
|   600 = #2563eb — primary FILLS. White text on it is 5.2:1 (WCAG AA).
|   700 = #1d4ed8 — hover/pressed.
| 50/100 are pale surfaces (bg-brand-50 backs ~900 usages, so it has to stay
| quiet), 300 is for hairline borders.
|
| Charts, maps and canvases that cannot use utilities carry these hexes as
| literals (utils/chartColors.js, utils/aeCharts.js, ProjectDetail/*.css, the
| maintenance notice, PlaybookCatalog's group accent). Change the ramp here and
| replace those literals in the same commit — a stop-by-stop find-and-replace
| is what moved them from red to blue.
*/
@theme {
    --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;

    --color-brand-50: #eff6ff;
    --color-brand-100: #dbeafe;
    --color-brand-200: #bfdbfe;
    --color-brand-300: #93c5fd;
    --color-brand-400: #60a5fa;
    --color-brand-500: #3b82f6;
    --color-brand-600: #2563eb;
    --color-brand-700: #1d4ed8;
    --color-brand-800: #1e40af;
    --color-brand-900: #1e3a8a;

    --color-navy-600: #3a3a3f;
    --color-navy-700: #2a2a2e;
    --color-navy-800: #1f1f23;
    --color-navy-900: #141417;
    --color-navy-950: #0a0a0b;

    /* Ink & Indigo — Analyze Property semantic tokens (not in stock Tailwind).
       Indigo/emerald/amber/red come from stock Tailwind utilities; brand/navy are untouched. */
    --color-canvas: #f4f5f8;   /* cool page background — white cards are islands on it */
    --color-hairline: #e6e8ee; /* use sparingly; prefer shadow + radius over borders */
}

@layer base {
    html {
        font-family: var(--font-sans);
        -webkit-font-smoothing: antialiased;
        text-rendering: optimizeLegibility;
    }
}

/*
|--------------------------------------------------------------------------
| Chinese typography — global
|--------------------------------------------------------------------------
| Latin type settings do not survive translation, and every page on this app
| can be served in Chinese (SetPublicSite / SetLocaleTimezone stamp the locale
| on <html lang>, so `zh_HK` and `zh_CN` both match `[lang^="zh"]`). The four
| corrections below are the ones that are ALWAYS wrong for Chinese, whatever
| the page — which is why they live here and not in one page's <style> block:
|
|   1. FONT. Inter has no CJK glyphs, so without a stack every Chinese page
|      falls to whatever the OS picks — often a serif on Windows, a different
|      face on Android. Latin faces stay FIRST in the list: font-family falls
|      back per glyph, so "MM2H860" renders in Inter and 星匯 in Noto Sans HK,
|      inside the same line.
|   2. LETTER-SPACING. `tracking-tight` (-0.025em) is a Latin display trick;
|      applied to square CJK glyphs it collides the strokes of adjacent
|      characters. Chinese wants 0 or slightly positive, never negative.
|   3. LINE-HEIGHT. Chinese glyphs have no descenders to create optical
|      space and fill the full em box, so Latin display leading (1.0–1.1)
|      makes consecutive lines touch. Headings need ~1.3, body ~1.85.
|   4. SYNTHETIC BOLD. `font-weight: 800` on a CJK face that only ships 400
|      makes the browser smear the glyphs. We load 700/800 of Noto Sans HK so
|      the weight is real (SiteLayout's Google Fonts request).
|
| These rules are intentionally UNLAYERED: Tailwind's utilities sit in
| @layer utilities, and an unlayered rule outranks any layered one, so
| `tracking-tight` / `leading-[1.02]` in a page's markup lose to them for
| Chinese and keep working untouched for English.
*/
html[lang^="zh"] {
    --font-cjk: "Noto Sans HK", "PingFang HK", "PingFang SC", "Hiragino Sans GB",
        "Microsoft JhengHei", "Microsoft YaHei", "Noto Sans CJK HK", sans-serif;
}

html[lang^="zh"] body {
    font-family: "Inter", ui-sans-serif, system-ui, -apple-system, var(--font-cjk);
}

html[lang^="zh"] h1,
html[lang^="zh"] h2,
html[lang^="zh"] h3,
html[lang^="zh"] h4,
html[lang^="zh"] h5,
html[lang^="zh"] h6 {
    letter-spacing: normal;
}

/* Chinese has no inter-word spaces to absorb a long line, so the reading
   rhythm comes entirely from leading. Scoped to the marketing site, where the
   text is prose; dense admin tables keep their compact Latin metrics. */
html[lang^="zh"] .pl-site h1 {
    line-height: 1.22;
}

html[lang^="zh"] .pl-site h2,
html[lang^="zh"] .pl-site h3 {
    line-height: 1.34;
}

html[lang^="zh"] .pl-site p,
html[lang^="zh"] .pl-site li {
    line-height: 1.85;
}

html[lang^="zh"] .pl-site .font-display {
    font-family: "Plus Jakarta Sans", "Inter", var(--font-cjk);
}

/*
|--------------------------------------------------------------------------
| Animations
|--------------------------------------------------------------------------
*/
@keyframes pl-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes pl-fade-up {
    from { opacity: 0; transform: translateY(14px); }
    to { opacity: 1; transform: translateY(0); }
}

@keyframes pl-float {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-12px); }
}

@keyframes pl-pulse-ring {
    0% { transform: scale(0.95); opacity: 0.5; }
    50% { transform: scale(1.05); opacity: 0.2; }
    100% { transform: scale(0.95); opacity: 0.5; }
}

.animate-fade-in { animation: pl-fade-in 0.5s ease both; }
.animate-fade-up { animation: pl-fade-up 0.55s cubic-bezier(0.22, 1, 0.36, 1) both; }
.animate-float { animation: pl-float 7s ease-in-out infinite; }
.animate-pulse-ring { animation: pl-pulse-ring 4s ease-in-out infinite; }

/*
|--------------------------------------------------------------------------
| Radar search motif (Analyze Property)
|--------------------------------------------------------------------------
| Ported from petav2 index.blade.php L36-66, blue→indigo for cohesion.
| Used by the searched-location pin and the comparison maps.
*/
@keyframes radar-ping {
    0%   { transform: scale(0.6); opacity: 0.7; }
    100% { transform: scale(2.4); opacity: 0; }
}
@keyframes radar-rotate {
    to { transform: rotate(360deg); }
}
.animate-radar-ping  { animation: radar-ping 1.5s ease-out infinite; }
.animate-radar-sweep { animation: radar-rotate 3s linear infinite; }

/* Spinner for an in-progress step (Analyze loading overlay). */
@keyframes ai-spin { to { transform: rotate(360deg); } }

/*
|--------------------------------------------------------------------------
| Sleek scrollbar
|--------------------------------------------------------------------------
| A slim, rounded, low-contrast scrollbar for dark surfaces (e.g. the
| sidebar nav). The track stays transparent and the thumb brightens on
| hover. Apply with the `scrollbar-sleek` class on any scroll container.
*/
.scrollbar-sleek {
    scrollbar-width: thin;
    scrollbar-color: rgba(148, 163, 184, 0.25) transparent;
}
.scrollbar-sleek::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}
.scrollbar-sleek::-webkit-scrollbar-track {
    background: transparent;
}
.scrollbar-sleek::-webkit-scrollbar-thumb {
    background-color: rgba(148, 163, 184, 0.25);
    border-radius: 9999px;
}
.scrollbar-sleek::-webkit-scrollbar-thumb:hover {
    background-color: rgba(148, 163, 184, 0.45);
}

[x-cloak] { display: none !important; }

/*
 * "Found it" — the moment the library assistant points at a resource.
 *
 * The job is ATTENTION, and attention is comparative: a ring drawn on one card
 * competes with forty other cards drawn exactly as loudly. So the effect works
 * on the whole grid, not on one element. Everything except the found card
 * recedes, and the found card gains a light that travels its edge. That reads in
 * peripheral vision, which is the actual test — the member is looking at the
 * chat panel when this fires, not at the grid.
 *
 * All of it is compositor-friendly: opacity, filter and a custom property. No
 * layout property is animated, so a grid of forty cards does not reflow while
 * the member is reading.
 */

/* The travelling light needs an ANGLE it can animate, and a raw custom property
   is not interpolatable — `--angle: 0deg` to `360deg` would jump, not sweep.
   Registering its type is what makes the sweep possible in pure CSS. Where
   @property is unsupported the ring simply renders static, which still reads as
   a highlight rather than as nothing. */
@property --ai-found-angle {
    syntax: '<angle>';
    initial-value: 0deg;
    inherits: false;
}

/* ── the grid recedes ──────────────────────────────────────────────────── */
.ai-learning-spotlight > li:not(.ai-learning-found) {
    /* Blur AND desaturate, not just dim. Dimming alone still leaves forty
       legible titles competing for the eye; taking the edges and the colour out
       of them is what makes one card the only thing in focus. */
    filter: blur(2px) saturate(0.45);
    opacity: 0.4;
    transition: filter 420ms cubic-bezier(0.22, 1, 0.36, 1),
                opacity 420ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* Returning is slower than leaving. A snap back is startling; a settle reads as
   the page relaxing once you have seen what you were shown. */
.ai-learning-grid > li {
    transition: filter 620ms cubic-bezier(0.22, 1, 0.36, 1),
                opacity 620ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* ── the found card ────────────────────────────────────────────────────── */
.ai-learning-found {
    position: relative;
    z-index: 1; /* above its dimmed neighbours, and above their blur */
    animation: ai-found-lift 2600ms cubic-bezier(0.22, 1, 0.36, 1) 1;
}

/* The light itself: a conic gradient painted only in a 2px band around the
   card. The band is cut with a two-layer mask composited to exclude — the
   inside is masked away, leaving the border. Rotating the ELEMENT would not
   work: the mask turns with it and a rotated rounded rectangle stops matching
   the card, which is why the gradient's start angle sweeps instead. */
.ai-learning-found::after {
    content: '';
    position: absolute;
    inset: -2px;
    border-radius: 1.1rem;
    padding: 2px;
    background: conic-gradient(
        from var(--ai-found-angle),
        transparent 0deg 200deg,
        var(--color-brand-300) 260deg,
        #ffffff 292deg,
        var(--color-brand-500) 320deg,
        transparent 360deg
    );
    -webkit-mask:
        linear-gradient(#000 0 0) content-box,
        linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask:
        linear-gradient(#000 0 0) content-box,
        linear-gradient(#000 0 0);
    mask-composite: exclude;
    pointer-events: none;
    animation: ai-found-sweep 1300ms linear 2,
               ai-found-fade 2600ms cubic-bezier(0.22, 1, 0.36, 1) 1;
}

/* A soft brand halo underneath, so the card reads as lit rather than outlined.
   Separate element from the ring because the two have different jobs and
   different timings — the halo arrives once and decays, the light goes round. */
.ai-learning-found::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 1rem;
    box-shadow: 0 0 0 0 rgb(37 99 235 / 0.45);
    pointer-events: none;
    animation: ai-found-halo 2600ms cubic-bezier(0.22, 1, 0.36, 1) 1;
}

@keyframes ai-found-sweep {
    to { --ai-found-angle: 360deg; }
}

@keyframes ai-found-fade {
    0% { opacity: 0; }
    10% { opacity: 1; }
    82% { opacity: 1; }
    100% { opacity: 0; }
}

@keyframes ai-found-halo {
    0% { box-shadow: 0 0 0 0 rgb(37 99 235 / 0.5), 0 0 0 0 rgb(37 99 235 / 0); }
    18% { box-shadow: 0 0 24px 4px rgb(37 99 235 / 0.28), 0 0 0 0 rgb(37 99 235 / 0); }
    70% { box-shadow: 0 0 18px 2px rgb(37 99 235 / 0.12), 0 0 0 0 rgb(37 99 235 / 0); }
    100% { box-shadow: 0 0 0 0 rgb(37 99 235 / 0), 0 0 0 0 rgb(37 99 235 / 0); }
}

/* Barely a lift. Enough to read as "this one came forward", small enough that
   the card never covers its neighbours or drags the grid's rhythm out of true. */
@keyframes ai-found-lift {
    0% { transform: scale(1); }
    14% { transform: scale(1.022); }
    100% { transform: scale(1); }
}

/*
 * Reduced motion keeps the SPOTLIGHT and drops the movement. The recede is a
 * state change rather than a journey, so it is still legible without animating,
 * and the found card keeps a static ring. Removing the whole effect here would
 * leave the assistant pointing at nothing for the members who most need to be
 * told where to look.
 */
@media (prefers-reduced-motion: reduce) {
    .ai-learning-spotlight > li:not(.ai-learning-found),
    .ai-learning-grid > li {
        transition: none;
    }

    .ai-learning-found {
        animation: none;
    }

    .ai-learning-found::after {
        animation: none;
        background: var(--color-brand-500);
    }

    .ai-learning-found::before {
        animation: none;
        box-shadow: 0 0 0 4px rgb(37 99 235 / 0.18);
    }
}

/*
|--------------------------------------------------------------------------
| Print — "Download PDF" is the browser's own print-to-PDF
|--------------------------------------------------------------------------
| The Wealth Planning result page is a REPORT, and members asked to keep a
| copy. Every figure on it is computed in the browser (the controller only
| persists the plan's state blob), so a server-rendered PDF would have to
| duplicate the whole projection engine — the one thing the module's handbook
| forbids. Printing the page the member is looking at is therefore not the
| cheap option, it is the only one that cannot disagree with the screen.
|
| It is also better output than a canvas-scraping library would give: text
| stays selectable and the file stays small. html2canvas was ruled out for a
| harder reason — Tailwind v4 emits `oklch()` colours, which it cannot parse.
|
| These rules are global because the chrome they hide (sidebar, sticky header)
| belongs to AppShell, and no page should print its own navigation.
*/
@media print {
    /* App chrome and anything interactive is not part of a document. */
    .print-hide,
    .print-hide * {
        display: none !important;
    }

    /* Screen-only helpers that must appear ON PAPER (cover block, CTA). */
    .print-only {
        display: block !important;
    }

    /* The sidebar is gone, so its gutter must go with it. */
    .app-shell-content {
        padding-left: 0 !important;
    }

    /* Backgrounds and brand colours are meaningful here (green = rent covers
       the loan), so keep them rather than letting the browser drop them. */
    * {
        -webkit-print-color-adjust: exact !important;
        print-color-adjust: exact !important;
    }

    html,
    body {
        background: #fff !important;
    }

    /* A report is read section by section; let each start cleanly and never
       split a property card or a stat across two sheets. */
    .print-section {
        break-inside: avoid;
        page-break-inside: avoid;
    }

    .print-break-before {
        break-before: page;
        page-break-before: always;
    }

    /* Nothing sticky or scrolling survives on paper. */
    .sticky,
    .fixed {
        position: static !important;
    }

    [class*="overflow-"] {
        overflow: visible !important;
    }

    /* Shadows print as grey mud on most drivers. */
    * {
        box-shadow: none !important;
    }

    @page {
        margin: 14mm 12mm;
    }
}

/* The print-only blocks must not show on screen. `.print-only` is flipped to
   visible inside the print block above. */
.print-only {
    display: none;
}
