/* ============================================================================
 * modal-foundation.css — Canonical FFV modal foundation tokens + base classes.
 *
 * Built by modal-integrity-scan S3 (2026-06-02). ADDITIVE: defines the shared
 * tokens and `.ffa-modal` base that S4–S6 migrate modals onto. No existing modal
 * rule is altered here (the one in-place edit this session — the close-button
 * `transition: all` fix — lives in styles.css where those classes are defined).
 *
 * Implements docs/MODAL_STANDARD.md §4 (visual/CSS/mobile contract).
 * ========================================================================== */

:root {
    /* --- Animation tokens (MODAL_STANDARD §4.2) --- */
    --modal-enter-duration: 200ms;
    --modal-exit-duration: 150ms;
    --modal-ease: cubic-bezier(0.2, 0, 0, 1);

    /* --- Z-index tier completion (MODAL_STANDARD §4.3) ---
     * NEW vars mapping the existing raw values already in use across CSS/JS.
     * These DO NOT renumber anything — they give the scattered literals a name
     * so migrations replace `z-index: 10400` with `z-index: var(--z-modal-umi)`.
     * The four base tiers (--z-modal-backdrop/-primary/-secondary/-tertiary),
     * --z-fullscreen, --z-system, --z-critical already live in styles.css :root. */
    --z-modal-curator: 10100;      /* person / curator overlays */
    /* 10200 (--z-modal-single-photo) retired with the single-photo Face Tagging
       modal — forging-universal-enrichment S3; the universal enrichment modal
       never stacks above the Review grid (one surface at a time). */
    --z-modal-umi: 10400;          /* Unified Media Modal (UMI Heart) */
    --z-mobile-drawer: 12000;      /* hamburger drawer (above TabBar) */
    --z-modal-fullscreen: 15000;   /* alias of --z-fullscreen — story takeover */
    --z-modal-ai: 20000;           /* AI fullscreen */
    --z-modal-focus: 30000;        /* focus pickers (30000–30002) */
    --z-modal-system: 100000;      /* context modal, story-gen */
    --z-modal-critical: 100001;    /* user dropdown — always-top */
}

/* --- Body scroll-lock (MODAL_STANDARD §2, ref-counted by ModalManager) ---
 * position:fixed defeats iOS rubber-band scroll-through; the saved scrollY is
 * applied as a negative top offset so the page does not visually jump. The JS
 * util restores window.scrollTo(0, savedY) when the last modal closes. */
body.modal-scroll-lock {
    position: fixed;
    top: var(--ffa-locked-scroll-y, 0);
    left: 0;
    right: 0;
    width: 100%;
    overflow: hidden;
}

/* --- Canonical overlay base (MODAL_STANDARD §1.2 / §4.2) ---
 * Migrations add `.ffa-modal` (+ a type modifier) to the overlay root ALONGSIDE
 * the modal's existing per-modal prefix class. Visibility + animation are driven
 * off `.is-open` (written only by ModalManager). Compositor-only: opacity on the
 * overlay, transform on the panel. No layout/paint-triggering properties. */
.ffa-modal {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal-primary);
    display: flex;
    align-items: center;
    justify-content: center;
    /* The class answer to G-160: a panel taller than its overlay was centred, which put its header — the title and
       the ✕ — above the overlay's top edge, under the phone's band. `safe` centring falls back to the start edge
       on overflow, so a panel's top (and its way out) can never leave the screen. Older engines ignore the line
       and keep plain centring. */
    align-items: safe center;
    justify-content: safe center;
    opacity: 0;
    visibility: hidden;
    transition:
        opacity var(--modal-exit-duration) var(--modal-ease),
        visibility 0s linear var(--modal-exit-duration);
}

.ffa-modal.is-open {
    opacity: 1;
    visibility: visible;
    transition: opacity var(--modal-enter-duration) var(--modal-ease);
}

/* Docked to the Activity Dock (S6, Track B4): the modal stays OPEN (state,
 * content, FFASpatial geometry all live) but leaves the stage. Written only
 * by ModalManager.minimize()/restoreMinimized(). Compositor-only, same
 * fade discipline as .is-open. */
.ffa-modal.is-open.is-docked {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition:
        opacity var(--modal-exit-duration) var(--modal-ease),
        visibility 0s linear var(--modal-exit-duration);
}

/* THE SAME INVARIANT, FOR ROOTS THAT NEVER ADOPTED .ffa-modal.
 *
 * "Docked leaves the stage" is a property of the DOCKED STATE, not of the
 * foundation class — but it was only ever expressed on `.ffa-modal`, so every
 * ModalManager-managed root that is not foundation-classed kept its window
 * painted on the canvas while its Activity Dock chip appeared beside it. Two
 * such roots exist today: the room-window overlays (`.ffrw-overlay`, a
 * deliberately transparent stage rather than a scrim, so it must NOT become an
 * .ffa-modal) and `#faceTrainingModal` (`.modal face-training-modal`).
 *
 * Joe /listen 2026-07-31: minimizing Portraits docked it correctly but left the
 * Curator's Rail standing on the canvas behind Stories. Surfaced the moment CP
 * activation began parking rooms, which made minimize a common act rather than
 * a rare one.
 *
 * Keying on `.is-docked` alone is precise, not broad: that class is written
 * ONLY by ModalManager.minimize()/restoreMinimized() (see the note above), so
 * nothing else in the vault can wear it. The rule above keeps its transition
 * discipline for foundation-classed modals; this one guarantees the invariant
 * for every managed root, including ones added later.
 *
 * Joe confirmed the flaw is UNIVERSAL — "minimize has that flaw in every case"
 * — which is why this rule carries !important. Docked is not a style
 * suggestion competing with a component's own presentation; it is a statement
 * that the surface has left the stage, and a room that paints itself over the
 * canvas after the manager has docked it is simply wrong. `.is-docked` is
 * manager-private, so nothing legitimate is being overridden. Geometry is
 * deliberately preserved (visibility, not display) — the modal is still OPEN
 * and FFASpatial's rect must survive to restore it where the user left it. */
.is-docked {
    opacity: 0 !important;
    visibility: hidden !important;
    pointer-events: none !important;
}

/* Optional rise on the content panel. Migrations add `.ffa-modal__panel` to the
 * modal's content container to get fade+rise; without it the overlay still fades
 * (opacity-only is always conformant). Transform-only — compositor-safe. */
.ffa-modal__panel {
    transform: translateY(8px);
    transition: transform var(--modal-exit-duration) var(--modal-ease);
    will-change: transform;
}
.ffa-modal.is-open .ffa-modal__panel {
    transform: translateY(0);
    transition: transform var(--modal-enter-duration) var(--modal-ease);
}

/* --- Type modifiers (MODAL_STANDARD §4.5) --- */
.ffa-modal--sheet { align-items: flex-end; justify-content: stretch; }
.ffa-modal--sheet .ffa-modal__panel {
    width: 100%;
    padding-bottom: env(safe-area-inset-bottom, 0);
    transform: translateY(100%);
}
.ffa-modal--sheet.is-open .ffa-modal__panel { transform: translateY(0); }

.ffa-modal--drawer { align-items: stretch; justify-content: flex-start; }
.ffa-modal--lightweight { /* centered overlay; inherits base */ }

/* Full-viewport scroll takeover (UMI, Story detail, Browse Media). NOT centered:
 * overrides the flex base to a block scroll container so tall content scrolls
 * inside the fixed overlay. Visibility still fades via .is-open (opacity/visibility
 * — compositor-safe). The standard (§4.5) names full-screen takeover as a type;
 * added in S4 (modal-integrity-scan) when the first fullscreen modals migrated. */
.ffa-modal--fullscreen {
    display: block;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
}
.ffa-modal--fullscreen .ffa-modal__panel,
.ffa-modal--fullscreen.is-open .ffa-modal__panel {
    transform: none; /* no rise for a full-viewport takeover; fade only */
}

/* Z-index tier modifiers — let a migration set the tier in markup rather than
 * a per-modal CSS override (still resolves to the same reconciled values). */
.ffa-modal--tier-secondary  { z-index: var(--z-modal-secondary); }
.ffa-modal--tier-tertiary   { z-index: var(--z-modal-tertiary); }
.ffa-modal--tier-curator    { z-index: var(--z-modal-curator); }
.ffa-modal--tier-umi        { z-index: var(--z-modal-umi); }
.ffa-modal--tier-fullscreen { z-index: var(--z-modal-fullscreen); }
.ffa-modal--tier-ai         { z-index: var(--z-modal-ai); }
.ffa-modal--tier-focus      { z-index: var(--z-modal-focus); }
.ffa-modal--tier-system     { z-index: var(--z-modal-system); }
.ffa-modal--tier-critical   { z-index: var(--z-modal-critical); }

/* --- Reduced-motion: honor the OS preference (and sidestep any animation
 * cost entirely on constrained devices). --- */
@media (prefers-reduced-motion: reduce) {
    .ffa-modal,
    .ffa-modal.is-open,
    .ffa-modal__panel,
    .ffa-modal.is-open .ffa-modal__panel {
        transition-duration: 0.01ms !important;
    }
}

/* ── Surfaces that live in the App Panel (R17, settling-prerelease-rulings S4) ─────────────────
   Joe 2026-10-19, the window-frame rule: "Header + Control Panel always visible" and "nav
   activation closes modals". A surface listed here keeps its own geometry but, on the desktop,
   its overlay is CLIPPED to the App Panel: a clipped region takes no pointer, so the header and
   every Control Panel item stay live while it is open, and a press reaches navigateTo, whose
   closeAllModals() closes it (ModalManager.closeAll() for registered roots; script.js names the
   bespoke Living Portrait). The defect this closes: each of these sits ABOVE the side-nav
   (z 10001) with `inset: 0`, so a Control Panel press hit the transparent overlay or backdrop,
   closed the surface by click-outside and went nowhere; the member pressed twice.
   CLIP, NEVER MOVE: the folio's shell is FFASpatial-placed in viewport coordinates and the
   overlay is its containing block, so moving the overlay (top/left) applied the offset twice and
   put the ✕ off-screen. Clipping changes hit-testing and paint, not layout.
   ADD A SURFACE only when its content panel is known to sit right of the band (a full-screen
   viewer would lose controls under the clip) and nav provably closes it; prove both with
   work/settling-prerelease-rulings/s4/tools/r17_probe.py. The class census and the surfaces not
   (yet) listed, with why: work/settling-prerelease-rulings/S4_CLOSE.md § R17.
   `--side-nav-width` is the Control Panel band on the desktop only (the phone redeclares it as its
   drawer width), hence the 769px floor; FrameController's maximize zeroes both tokens, so a
   maximized pane is unclipped; a presentation-root page has no frame to keep. */
@media (min-width: 769px) {
    #biographyModalOverlay,                 /* the biography folio */
    .bkg-overlay:not(.bkg-mobile),          /* the Living Portrait (desktop card; the phone sheet is untouched) */
    #bioAttributionModal {                  /* Sources & Verification (Portraits tile, folio) */
        clip-path: inset(var(--nav-height, 60px) 0 0 var(--side-nav-width, 96px));
    }
    body.presentation-root #biographyModalOverlay,
    body.presentation-root .bkg-overlay:not(.bkg-mobile),
    body.presentation-root #bioAttributionModal {
        clip-path: none;
    }
}


/* ── Overlays SEATED in the App Panel (settling-modal-residue S2, D2 — RULINGS_OWED T9) ──────
   The same Window-Frame rule as the R17 block above, for three overlays that were never
   ModalManager members: Desktop Import, Media Profiles and the Face Sheet (cluster sheet).
   Each is now registered (Escape, and nav activation closes it through closeAll), and here
   its full-viewport overlay is clipped to the App Panel so the header and every Control
   Panel item stay live while it is open.
   CLIP, AND PAD — NEVER MOVE. Unlike the folio, these panels are centred by their overlay's
   flex box, so a bare clip would hide whatever of the panel lies under the band (the Face
   Sheet's title and first column always; Desktop Import's left 100 px at 820 px wide). The
   overlay's PADDING takes the band instead, so the panel centres in the App Panel. Padding
   does not move the overlay box, so a Stripped fill (FFASpatial: position:fixed in
   viewport coordinates, and Desktop Import's backdrop-filter makes its overlay the
   containing block) lands exactly where it did. ID selectors: Desktop Import injects its
   own stylesheet at open, which would win a class-for-class tie.
   NOT HERE, ON PURPOSE: the two profile selectors and a Desktop Import with an upload in
   flight (`.di-committing`) are deliberate BLOCKERS (S4_CLOSE § R17): a chooser the member
   answers or dismisses, and an upload whose queue a close would lose. The curator Context
   pop-up needs nothing: its overlay takes no pointer (ccx-popover).
   Proof: tests/settling-modal-residue/d2_overlays.mjs (Chromium + WebKit, served origin). */
@media (min-width: 769px) {
    /* body:not(.presentation-root): a presentation-root page has no frame to keep. */
    body:not(.presentation-root) #desktopImportOverlay,
    body:not(.presentation-root) #mediaProfilesModalOverlay,
    body:not(.presentation-root) #clusterSheetOverlay {
        box-sizing: border-box;
    }
    body:not(.presentation-root) #desktopImportOverlay:not(.di-committing),
    body:not(.presentation-root) #mediaProfilesModalOverlay,
    body:not(.presentation-root) #clusterSheetOverlay {
        clip-path: inset(var(--nav-height, 60px) 0 0 var(--side-nav-width, 96px));
    }
    body:not(.presentation-root) #desktopImportOverlay {
        padding: calc(var(--nav-height, 60px) + 24px) 24px 24px calc(var(--side-nav-width, 96px) + 24px);
    }
    body:not(.presentation-root) #desktopImportOverlay .di-card {
        max-width: 100%;
    }
    body:not(.presentation-root) #mediaProfilesModalOverlay {
        padding-left: calc(var(--side-nav-width, 96px) + var(--space-md, 16px));
        padding-top: max(8vh, calc(var(--nav-height, 60px) + var(--space-md, 16px)));
    }
    body:not(.presentation-root) #clusterSheetOverlay {
        padding: var(--nav-height, 60px) 0 0 var(--side-nav-width, 96px);
    }
}


/* ── The three operator SYSTEM overlays, seated in the App Panel (routing-admin-dialogs S1) ──
   Coordination Monitor, Representation Overview and Sensei Activity History are registered
   ModalManager members (tier 'system'); the window manager seats them in the band (z 10310+),
   above the side-nav (z 10001), and each paints its own `inset: 0` scrim. So a Control Panel
   press hit the scrim and went nowhere (measured 2026-10-21, Chromium 1440×900:
   elementFromPoint at a Control Panel item returned #coordinationMonitorOverlay). Same rule,
   same method as the block above: CLIP, AND PAD — NEVER MOVE. The panels are centred by the
   overlay's flex box, so the padding takes the band and the panel centres in the App Panel;
   the clip leaves the header and every Control Panel item live, and a press reaches
   navigateTo, whose closeAll() closes the overlay. ID selectors: each module injects its own
   stylesheet at open (its padding would win a class-for-class tie).
   Proof: work/routing-admin-dialogs/proof/s1_dialogs.mjs (Chromium + WebKit, served origin). */
@media (min-width: 769px) {
    body:not(.presentation-root) #coordinationMonitorOverlay,
    body:not(.presentation-root) #representationOverviewOverlay,
    body:not(.presentation-root) #senseiActivityOverlay {
        box-sizing: border-box;
        clip-path: inset(var(--nav-height, 60px) 0 0 var(--side-nav-width, 96px));
        padding: calc(var(--nav-height, 60px) + 24px) 16px 24px calc(var(--side-nav-width, 96px) + 16px);
    }
    body:not(.presentation-root) #coordinationMonitorOverlay > .coord-mon-modal,
    body:not(.presentation-root) #representationOverviewOverlay > .repr-ov-modal,
    body:not(.presentation-root) #senseiActivityOverlay > .sa-modal {
        max-width: 100%;
    }
}
