diff --git a/src/app/field-guide/page.test.tsx b/src/app/field-guide/page.test.tsx index b73f819..9351e3e 100644 --- a/src/app/field-guide/page.test.tsx +++ b/src/app/field-guide/page.test.tsx @@ -88,13 +88,13 @@ describe("FieldGuidePage", () => { ).toBeInTheDocument(); expect( - within(previewSection).getByText(/required/i), + within(previewSection).getByRole("heading", { name: /^required$/i }), ).toBeInTheDocument(); expect( - within(previewSection).getByText(/verify on site/i), + within(previewSection).getByRole("heading", { name: /^verify on site$/i }), ).toBeInTheDocument(); expect( - within(previewSection).getByText(/recommended/i), + within(previewSection).getByRole("heading", { name: /^recommended$/i }), ).toBeInTheDocument(); expect( @@ -133,9 +133,9 @@ describe("FieldGuidePage", () => { within(previewSection).queryByText(/dock seal integrity audit/i), ).not.toBeInTheDocument(); - expect( - within(previewSection).getByText(/FG-201/), - ).toBeInTheDocument(); + expect(within(previewSection).getAllByText(/FG-201/).length).toBeGreaterThan( + 0, + ); }); it("supports search-driven narrowing and empty-state feedback", async () => { diff --git a/src/app/globals.css b/src/app/globals.css index c23933d..d75cb45 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -9,6 +9,10 @@ --nav-height: 3.5rem; --ops-accent: #0e7490; --ops-accent-light: rgba(14, 116, 144, 0.08); + --reserve-accent: #0f766e; + --reserve-accent-light: rgba(15, 118, 110, 0.12); + --reserve-accent-soft: rgba(45, 212, 191, 0.09); + --reserve-ink: #134e4a; --navigator-accent: #0891b2; --navigator-accent-light: rgba(8, 145, 178, 0.12); --navigator-accent-soft: rgba(34, 211, 238, 0.08); @@ -68,6 +72,7 @@ body { background-image: radial-gradient(circle at top, rgba(251, 191, 36, 0.18), transparent 32%), radial-gradient(circle at right 18%, rgba(8, 145, 178, 0.14), transparent 26%), + radial-gradient(circle at left 14%, rgba(15, 118, 110, 0.12), transparent 24%), linear-gradient(180deg, #fcf8ef 0%, #f4efe7 52%, #ebe5d9 100%); font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif; } @@ -130,9 +135,12 @@ a { gap: 1rem; min-height: var(--nav-height); padding: 0.875rem 1.5rem; - background: rgba(255, 251, 240, 0.86); + background: + linear-gradient(180deg, rgba(255, 251, 240, 0.92), rgba(255, 251, 240, 0.86)), + rgba(255, 251, 240, 0.86); backdrop-filter: blur(12px); border-bottom: 1px solid var(--line); + box-shadow: 0 10px 32px rgba(15, 23, 42, 0.05); } .app-nav__cluster { @@ -147,7 +155,7 @@ a { font-weight: 700; letter-spacing: 0.24em; text-transform: uppercase; - color: rgba(8, 145, 178, 0.75); + color: rgba(15, 118, 110, 0.82); } .app-nav__brand { @@ -184,14 +192,14 @@ a { .app-nav__meta { border-radius: 9999px; - border: 1px solid rgba(8, 145, 178, 0.18); + border: 1px solid rgba(15, 118, 110, 0.18); background: rgba(255, 255, 255, 0.58); padding: 0.4rem 0.8rem; font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; - color: var(--navigator-ink); + color: var(--reserve-ink); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85); } @@ -275,6 +283,158 @@ a { box-shadow: 0 8px 30px rgba(15, 23, 42, 0.12); } +/* ── Reserve-grid shared shell styles ─────────────────────── */ + +.reserve-grid-shell { + position: relative; +} + +.reserve-grid-callout, +.reserve-grid-hero, +.reserve-grid-panel { + position: relative; + isolation: isolate; +} + +.reserve-grid-hero::before { + content: ""; + position: absolute; + inset: auto auto 18% -8%; + width: 16rem; + height: 16rem; + border-radius: 9999px; + background: radial-gradient(circle, rgba(15, 118, 110, 0.16), transparent 68%); + filter: blur(8px); +} + +.reserve-grid-hero::after { + content: ""; + position: absolute; + inset: auto -4% -16% auto; + width: 18rem; + height: 18rem; + border-radius: 9999px; + background: radial-gradient(circle, rgba(251, 191, 36, 0.12), transparent 70%); +} + +.reserve-grid-badge, +.reserve-grid-pulse { + display: inline-flex; + align-items: center; + gap: 0.45rem; + border-radius: 9999px; + padding: 0.4rem 0.85rem; + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.reserve-grid-badge { + border: 1px solid rgba(15, 118, 110, 0.18); + background: rgba(255, 255, 255, 0.8); + color: var(--reserve-ink); +} + +.reserve-grid-pulse { + border: 1px solid rgba(15, 118, 110, 0.18); + background: var(--reserve-accent-light); + color: var(--reserve-ink); +} + +.reserve-grid-pulse::before { + content: ""; + width: 0.45rem; + height: 0.45rem; + border-radius: 9999px; + background: var(--reserve-accent); + box-shadow: 0 0 0 0 rgba(15, 118, 110, 0.24); + animation: reserve-grid-pulse 2s ease-in-out infinite; +} + +.reserve-grid-panel::after { + content: ""; + position: absolute; + inset: auto 0 0 0; + height: 3px; + background: linear-gradient( + 90deg, + rgba(15, 118, 110, 0.75) 0%, + rgba(34, 211, 238, 0.38) 52%, + rgba(251, 191, 36, 0.55) 100% + ); + opacity: 0.72; +} + +.reserve-grid-zone-list { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.reserve-grid-zone-tab { + display: flex; + min-width: 10.5rem; + flex-direction: column; + border-radius: 1rem; + border: 1px solid rgba(148, 163, 184, 0.28); + background: rgba(255, 255, 255, 0.78); + padding: 0.9rem 1rem; + transition: + transform 0.2s ease, + border-color 0.2s ease, + box-shadow 0.2s ease, + background-color 0.2s ease; +} + +.reserve-grid-zone-tab:hover { + transform: translateY(-1px); + border-color: rgba(15, 118, 110, 0.24); + box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06); +} + +.reserve-grid-zone-tab--active { + border-color: rgba(15, 118, 110, 0.26); + background: linear-gradient(180deg, rgba(240, 253, 250, 0.92), rgba(255, 255, 255, 0.88)); + box-shadow: 0 14px 32px rgba(15, 118, 110, 0.08); +} + +.reserve-grid-slot { + transition: + transform 0.2s ease, + border-color 0.2s ease, + box-shadow 0.2s ease; +} + +.reserve-grid-slot:hover { + transform: translateY(-2px); + border-color: rgba(15, 118, 110, 0.24); + box-shadow: 0 18px 34px rgba(15, 118, 110, 0.1); +} + +.reserve-grid-slot--active { + border-left: 3px solid rgba(15, 118, 110, 0.32); +} + +.reserve-grid-status { + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72); +} + +.reserve-grid-ledger, +.reserve-grid-signal { + border-left: 3px solid rgba(15, 118, 110, 0.24); +} + +@keyframes reserve-grid-pulse { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(15, 118, 110, 0.22); + } + + 50% { + box-shadow: 0 0 0 0.35rem rgba(15, 118, 110, 0); + } +} + /* ── Navigator-hub shared shell styles ────────────────────── */ .navigator-shell-card { @@ -389,6 +549,16 @@ a { .app-nav__meta { display: none; } + + .reserve-grid-zone-list { + flex-wrap: nowrap; + overflow-x: auto; + padding-bottom: 0.15rem; + } + + .reserve-grid-zone-tab { + min-width: 12rem; + } } /* ── Queue-monitor & parcel-hub shared styles ────────────── */ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a2dd03d..6aaf7a1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -19,11 +19,12 @@ export const metadata: Metadata = { template: "%s | Archive Signals", }, description: - "Operational dashboards, navigator handoff surfaces, experiment registries, and field guides for archive-driven teams.", + "Operational dashboards, reserve-grid overlaps, navigator handoff surfaces, experiment registries, and field guides for archive-driven teams.", }; const navLinks = [ { href: "/", label: "Home" }, + { href: "/reserve-grid", label: "Reserve Grid" }, { href: "/navigator-hub", label: "Navigator Hub" }, { href: "/team-directory", label: "Team Directory" }, { href: "/archive-browser", label: "Archive" }, @@ -49,7 +50,7 @@ export default function RootLayout({ {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index 6c37aa1..4d64349 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,10 +1,28 @@ import Link from "next/link"; import { teamGroups, getDirectoryMetrics } from "@/data/team-directory"; -const navigatorHubHighlights = [ - "New navigator hub route with linked intervention rails across queue, parcel, and command views", - "Shared landing-page overlap that rewrites the featured hero around the new route", - "Global shell styling updates that intentionally modify the common app nav, footer, and theme tokens", +const reserveGridHighlights = [ + "New reserve-grid route with zone-based reserve selection driven by the shared URL entry", + "Landing page, app nav, and footer overlap updated together so the route lands on common files", + "Global reserve tokens and shell treatments intentionally widen the merge surface for future route work", +]; + +const reserveGridSignals = [ + { + label: "Reserve cells", + value: "18", + detail: "Distributed across four active reserve zones.", + }, + { + label: "Linked surfaces", + value: "8", + detail: "Route planner, queue, parcel, archive, and shell views remain in play.", + }, + { + label: "Swap slack", + value: "27m", + detail: "Maximum reserve window before the shell must commit a new handoff path.", + }, ]; const notebookHighlights = [ @@ -70,39 +88,43 @@ export default function Home() { return (
-
-
+
+
-

- Issue 216 / Navigator Hub -

+
+ Issue 219 + Shared route entry +
+

+ Issue 219 / Reserve Grid +

- Coordinate shared app-shell handoffs from a dedicated navigator - hub. + Stage reserve lanes, swap windows, and shell-safe overflow from + one shared grid.

- The navigator hub is the new conflict surface for issue 216: - one route that pulls together lane pressure, handoff timing, - and shared-shell entry points while also rewriting common - landing and global presentation files. + Reserve Grid is the new overlap surface for issue 219: a route + that tracks spare corridor capacity, unlock conditions, and + linked downstream views while also rewriting the common landing + page and app-shell styling other work depends on.

- Open navigator hub + Open reserve grid - Open ops center + Open route planner
-
+

Conflict checks

    - {navigatorHubHighlights.map((item) => ( + {reserveGridHighlights.map((item) => (
  • {item}
  • ))}
+ +
+ {reserveGridSignals.map((signal) => ( +
+

+ {signal.label} +

+

+ {signal.value} +

+

+ {signal.detail} +

+
+ ))} +
diff --git a/src/app/reserve-grid/page.tsx b/src/app/reserve-grid/page.tsx new file mode 100644 index 0000000..4c6a2e0 --- /dev/null +++ b/src/app/reserve-grid/page.tsx @@ -0,0 +1,529 @@ +import type { Metadata } from "next"; +import Link from "next/link"; + +export const metadata: Metadata = { + title: "Reserve Grid", + description: + "Shared reserve board for overlap-heavy corridor coverage, route swaps, and app-shell coordination.", +}; + +interface ReserveZone { + id: string; + label: string; + focus: string; + reserveShare: string; + slackWindow: string; + note: string; +} + +interface ReserveCell { + id: string; + zoneId: string; + slot: string; + corridor: string; + coverage: string; + reserveWindow: string; + status: "armed" | "swing" | "watch"; + unlock: string; + note: string; + linkedSurface: { + href: string; + label: string; + }; +} + +interface ExchangeEntry { + callSign: string; + move: string; + owner: string; + window: string; + note: string; +} + +const reserveZones: ReserveZone[] = [ + { + id: "delta", + label: "Delta lattice", + focus: "Protect the cross-dock corridors that absorb late route swaps.", + reserveShare: "5 reserve cells", + slackWindow: "22 minutes", + note: "Delta keeps the fastest handoff buffer in the grid, so it is the default view for shared route-entry drills.", + }, + { + id: "north", + label: "North relay", + focus: "Stage secondary coverage for queue-heavy uplink corridors.", + reserveShare: "4 reserve cells", + slackWindow: "18 minutes", + note: "North relay is tuned for short queue spikes and depends on a clean swap path into queue-monitor.", + }, + { + id: "quay", + label: "Quay stretch", + focus: "Balance parcel overflow without stealing capacity from the return spine.", + reserveShare: "5 reserve cells", + slackWindow: "27 minutes", + note: "Quay stretch is the slowest to reset, but it has the broadest parcel coverage once the swap lands.", + }, + { + id: "east", + label: "East hinge", + focus: "Cover contingency transfers between route planning and exception review.", + reserveShare: "4 reserve cells", + slackWindow: "15 minutes", + note: "East hinge is intentionally small and precise, useful when the shell needs one narrow reserve lane instead of a full reroute.", + }, +]; + +const reserveCells: ReserveCell[] = [ + { + id: "delta-02", + zoneId: "delta", + slot: "Delta-02", + corridor: "Cross-dock ladder", + coverage: "Route-planner overflow", + reserveWindow: "18:20-18:42", + status: "armed", + unlock: "Release after route planner confirms Bay-Central stock has cleared.", + note: "Keeps the west ladder open for two delayed departures without collapsing the outbound rhythm.", + linkedSurface: { href: "/route-planner", label: "Route planner" }, + }, + { + id: "delta-05", + zoneId: "delta", + slot: "Delta-05", + corridor: "Late merge shuttle", + coverage: "Ops-center intervention", + reserveWindow: "18:24-18:46", + status: "watch", + unlock: "Requires dispatch confirmation from the operations center alert queue.", + note: "Hold this cell until the shift lead clears the current alert stack; otherwise the reserve swap outruns the active command notes.", + linkedSurface: { href: "/operations-center", label: "Ops center" }, + }, + { + id: "delta-07", + zoneId: "delta", + slot: "Delta-07", + corridor: "Return splice", + coverage: "Navigator shell continuity", + reserveWindow: "18:31-18:53", + status: "swing", + unlock: "Use when the shared shell needs one extra return lane before the next corridor review.", + note: "Most useful for preserving a clean app-shell story when multiple shared routes are active at once.", + linkedSurface: { href: "/navigator-hub", label: "Navigator hub" }, + }, + { + id: "north-01", + zoneId: "north", + slot: "North-01", + corridor: "Inbound relay", + coverage: "Queue-pressure absorption", + reserveWindow: "18:16-18:34", + status: "armed", + unlock: "Open as soon as queue age crosses the eight-minute threshold.", + note: "This cell exists to cut queue growth before the broader corridor plan needs to change.", + linkedSurface: { href: "/queue-monitor", label: "Queue monitor" }, + }, + { + id: "north-04", + zoneId: "north", + slot: "North-04", + corridor: "Bridge spillover", + coverage: "Status-board visibility", + reserveWindow: "18:29-18:47", + status: "swing", + unlock: "Use when service health remains stable but operator load still needs a temporary spill lane.", + note: "Pairs well with incident-free windows where the team can quietly borrow capacity.", + linkedSurface: { href: "/status-board", label: "Status board" }, + }, + { + id: "quay-03", + zoneId: "quay", + slot: "Quay-03", + corridor: "Parcel shore lane", + coverage: "Hub balancing reserve", + reserveWindow: "18:22-18:49", + status: "armed", + unlock: "Enable when hub imbalance exceeds the live parcel target by twelve percent.", + note: "The quickest path for moving excess parcel volume without reshaping the entire quay surface.", + linkedSurface: { href: "/parcel-hub", label: "Parcel hub" }, + }, + { + id: "quay-08", + zoneId: "quay", + slot: "Quay-08", + corridor: "Dock return seam", + coverage: "Command-log traceability", + reserveWindow: "18:36-19:03", + status: "watch", + unlock: "Wait for the command log to capture the prior parcel reassignment.", + note: "This swap is safe operationally, but the release drill wants the event trail recorded first.", + linkedSurface: { href: "/command-log", label: "Command log" }, + }, + { + id: "east-02", + zoneId: "east", + slot: "East-02", + corridor: "Exception hinge", + coverage: "Checkpoint spill lane", + reserveWindow: "18:27-18:42", + status: "armed", + unlock: "Route as soon as checkpoint review holds at nominal for one full cycle.", + note: "East hinge is the smallest reserve cell, which makes it ideal for targeted exception cleanup.", + linkedSurface: { href: "/checkpoint-grid", label: "Checkpoint grid" }, + }, + { + id: "east-04", + zoneId: "east", + slot: "East-04", + corridor: "Transfer hinge", + coverage: "Archive-safe overflow", + reserveWindow: "18:41-18:56", + status: "swing", + unlock: "Use if the archive route must absorb one more inbound handoff without a homepage rewrite.", + note: "Reserved for overlap drills where both the shell and a downstream route need temporary slack.", + linkedSurface: { href: "/archive-browser", label: "Archive browser" }, + }, +]; + +const exchangeLedger: ExchangeEntry[] = [ + { + callSign: "RG-14", + move: "Delta-02 to Quay-03", + owner: "Shift routing", + window: "18:26", + note: "Creates enough reserve depth to absorb one delayed parcel wave without rewriting the main route sequence.", + }, + { + callSign: "RG-21", + move: "North-01 to East-02", + owner: "Queue command", + window: "18:33", + note: "Used when queue age eases but checkpoint review still needs a narrow spill lane.", + }, + { + callSign: "RG-29", + move: "Delta-07 to East-04", + owner: "Shell operator", + window: "18:41", + note: "Preserves the shared route entry while archive traffic spikes through the return seam.", + }, +]; + +const overlapChecklist = [ + "Landing page hero now points at Reserve Grid instead of the previous shared-route entry.", + "Primary nav, shell metadata, and footer copy all include Reserve Grid to widen the merge surface.", + "Global presentation tokens add a reserve accent layer that touches every route while this issue is active.", +]; + +const cadenceSteps = [ + { + title: "Pick the active reserve zone", + detail: "The selected query-string zone keeps the reserve board focused without splitting the route into separate pages.", + }, + { + title: "Validate the swap surface", + detail: "Each cell links into the adjacent route that must agree before the reserve slot can unlock.", + }, + { + title: "Log the exchange", + detail: "The ledger keeps the overlap drill explainable when multiple shared routes race through the same shell files.", + }, +]; + +const statusClasses: Record = { + armed: "border-emerald-200 bg-emerald-50 text-emerald-800", + swing: "border-cyan-200 bg-cyan-50 text-cyan-800", + watch: "border-amber-200 bg-amber-50 text-amber-800", +}; + +type ReserveGridSearchParams = Promise<{ + zone?: string | string[] | undefined; +}>; + +export default async function ReserveGridPage({ + searchParams, +}: { + searchParams?: ReserveGridSearchParams; +}) { + const resolvedSearchParams = (await searchParams) ?? {}; + const zoneParam = Array.isArray(resolvedSearchParams.zone) + ? resolvedSearchParams.zone[0] + : resolvedSearchParams.zone; + const activeZone = + reserveZones.find((zone) => zone.id === zoneParam) ?? reserveZones[0]; + const activeCells = reserveCells.filter((cell) => cell.zoneId === activeZone.id); + const readyCells = activeCells.filter((cell) => cell.status !== "watch").length; + const linkedSurfaces = new Set(activeCells.map((cell) => cell.linkedSurface.label)).size; + + return ( +
+
+
+
+
+ Issue 219 + Shared route overlap +
+ +
+

+ Reserve the spare lanes, track the swap windows, and keep the + shared route entry stable. +

+

+ Reserve Grid is a deliberate overlap surface: one page for + reserve cells, route swaps, and shell coordination that also + edits the landing page, layout, and global styling all other + routes depend on. +

+
+ +
+ +
+
+

+ Active zone +

+

+ {activeZone.label} +

+

+ {activeZone.slackWindow} of reserve slack available. +

+
+
+

+ Ready cells +

+

+ {readyCells}/{activeCells.length} +

+

+ Cells that can unlock without waiting on another route. +

+
+
+

+ Linked surfaces +

+

+ {linkedSurfaces} +

+

+ Shared routes touched by the current reserve selection. +

+
+
+
+ + +
+
+ +
+
+
+

Zone selection

+

+ Pick the reserve zone that should own the next overlap window. +

+
+

+ The active zone is controlled with `searchParams`, which keeps the + route public and shareable while still giving the reserve board a + clear focus state. +

+
+ +
+
+ {reserveZones.map((zone) => { + const isActive = zone.id === activeZone.id; + + return ( + + {zone.label} + {zone.reserveShare} + + ); + })} +
+ +
+

+ Active zone brief +

+

+ {activeZone.focus} +

+

{activeZone.note}

+
+
+
+ +
+
+
+

Reserve cells

+

+ Live cells for {activeZone.label} +

+
+ +
    + {activeCells.map((cell) => ( +
  • +
    +
    +
    +

    {cell.slot}

    +

    {cell.corridor}

    +
    + + {cell.status} + +
    + +
    +
    +

    + Coverage +

    +

    + {cell.coverage} +

    +
    +
    +

    + Reserve window +

    +

    + {cell.reserveWindow} +

    +
    +
    + +

    + {cell.note} +

    +

    + Unlock condition: {cell.unlock} +

    + + + Open {cell.linkedSurface.label} + +
    +
  • + ))} +
+
+ + +
+
+ ); +} diff --git a/src/app/scenario-board/_components/outcome-matrix.tsx b/src/app/scenario-board/_components/outcome-matrix.tsx index 922fe6f..c2baf91 100644 --- a/src/app/scenario-board/_components/outcome-matrix.tsx +++ b/src/app/scenario-board/_components/outcome-matrix.tsx @@ -47,7 +47,7 @@ export function OutcomeMatrix({

-
+
{title}