Skip to main content
TRIFORCECRITICAL CARE · ANYWHERE
Cockpit instrument panel at dusk
Home

Documentation

Build notes.
v0.1.0.

Reference for the current build of the Triforce marketing & operations app. The version below is the single source of truth — it is read from package.json at build time, so bumping the package version automatically updates this page.

Current version
v0.1.0
From package.json
Package
triforce-app
Next.js application
Build date
2026-06-29
Rendered server-side

Versioning

One source
of truth.

  • The version lives in package.json and is re-exported from src/lib/version.ts.
  • To cut a new release, bump the version in package.json (e.g. npm version patch) and add an entry to CHANGELOG.md.
  • The number displayed on this page updates automatically on the next build.
  • We follow Semantic Versioning: MAJOR.MINOR.PATCH.

Changelog

Every change, shipped.

In progress

Unreleased

Added

1 entry
  • GCC & MENA Private Aviation Advisory hub (/private-jets/gcc-mena + /ar/private-jets/gcc-mena)

    New bilingual page targeting GCC/MENA buyers — the site's primary market. Covers GCC aircraft registry guidance (A6-, HZ-, A4O-, 9K- prefixes), SPV/holding-company structuring (Cayman, BVI, DIFC), Cape Town Convention protections, FBO profiles for six major GCC airports (OMDB, OERK, OMAA, OTHH, OOMS, OKBK), two inline case studies (UAE G700 brokerage at 14 % below list; MENA multi-stop roadshow), a six-question FAQ accordion, and a market-snapshot card. English and full MSA Arabic counterparts ship together. Route registered in AR_BUILT_ROUTES (middleware) and added to the docs/I18N.md coverage matrix.

Changed

2 entries
  • PERF: AVIF image format + tuned deviceSizes/imageSizes in next.config.ts

    Next.js defaults to WebP-only for optimised image delivery. Adding formats: ['image/avif', 'image/webp'] enables AVIF for Chrome 85+, Firefox 113+, and Safari 16.4+ — typically 30–50 % smaller than WebP for the photographic aircraft and interior imagery on this site. Also added breakpoint-tuned deviceSizes (390, 640, 768, 1024, 1280, 1536, 1920) and imageSizes (16–384 px) so the generated srcset buckets closely match actual render sizes at the three tiers defined in CLAUDE.md (mobile / tablet / desktop), eliminating over-serving on narrow viewports.

  • PERF: Blur placeholders on all major image components

    Every next/image <Image> component in the main marketing content was loading from a blank white/transparent box, then snapping to the photo once it arrived. On slow or congested connections this creates a jarring visual that a Lighthouse "avoid invisible text / layout shift" audit flags as a perceived-performance gap. Added placeholder="blur" and a shared blurDataURL (dark #0d0d14 SVG matching the site's near-black background, via src/lib/blur-placeholder.ts) to five components:

    • aircraft-card.tsx — fleet list and search result cards
    • jet-marquee.tsx — homepage marquee tile images
    • stacking-cards.tsx — homepage service verticals hero cards
    • scroll-story.tsx — scroll-driven air-ambulance journey (both the sticky desktop panel and the mobile per-chapter thumbnails)
    • aircraft-for-sale-board.tsx — brokerage listing photos

    The home-page PageHero already has its own warm-gradient blurDataURL (from PR #284); this PR covers every other image-bearing surface.

Fixed

5 entries
  • A11Y: Admin panel loading spinners now respect prefers-reduced-motion

    Two <Loader2 className="animate-spin" /> spinners in the integration config panel (Test and Save button states) were missing the motion-reduce:animate-none Tailwind variant. Users with prefers-reduced-motion: reduce set at the OS level would see continuously spinning icons — a WCAG 2.3.3 (Animation from Interactions) violation that would be flagged by an axe accessibility scan even on admin-only surfaces. Added motion-reduce:animate-none to both instances, consistent with every other loading spinner in the codebase (trip-builder.tsx, air-ambulance-intake.tsx, sim-loading-veil.tsx, account-actions.tsx, sign-in-form.tsx).

  • PERF: Lightbox fullscreen image — non-blocking decode + explicit intent

    The aircraft gallery lightbox (ZoomableImage) renders a raw <img> (intentionally, for native pan/zoom support) without loading or decoding attributes. The browser defaulted to synchronous image decoding on the main thread, blocking rendering for the duration of the decode step when a high-resolution photo was opened in full screen. Added decoding="async" so the browser can decode off the main thread without holding up layout, and loading="eager" to make the intent explicit (the image is shown immediately on user gesture, not lazily deferred).

  • PERF/A11Y: AR QR code <img> gains intrinsic dimensions

    The QR code rendered inside the aircraft detail panel (AR launch flow) was missing explicit width and height attributes, even though the image is always generated at 512 × 512 px. Added width={512} height={512} and decoding="async" — aligns with the existing aircraft-picker.tsx pattern, gives the browser an aspect-ratio hint before the data URL resolves, and avoids any residual layout-shift ambiguity.

  • A11Y: RTL marquee respects prefers-reduced-motion

    The Arabic site (/ar, dir="rtl") rendered a continuous 38-second horizontal marquee loop even when the user's OS had motion-reduction enabled. The root cause: a high-specificity RTL animation-name override in the global stylesheet sat outside the prefers-reduced-motion guard block, creating a cascade ambiguity. Added an explicit html[dir="rtl"] .ios-marquee { animation: none !important } inside a second @media (prefers-reduced-motion: reduce) block at the RTL override site to make the intent unambiguous and future-proof. WCAG 2.3.3 (Animation from Interactions).

  • PERF: Hero LCP image — warm blur placeholder on / and /ar

    The hero <Image> on both the English and Arabic home pages loaded with a blank white/black flash before the Unsplash photo appeared (the image is the Largest Contentful Paint element). Added placeholder="blur" with a hand-crafted blurDataURL — a tiny SVG linear gradient (dark-navy → deep-amber → near-black) that approximates the sunset-over-tarmac colour palette. The gradient fills instantly from cache, eliminating the blank pop-in on repeat visits and reducing perceived LCP by giving the eye something to anchor to while the full image loads. No layout shift; the fill container dimensions are unchanged.

Added

3 entries
  • Aircraft for Sale — /private-jets/for-sale and /ar/private-jets/for-sale

    New brokerage inventory board surfacing eight pre-owned and pre-delivery aircraft available through the Triforce network. This is the page that was structurally absent from the buyer journey: fleet pages show aircraft *types*, but there was nothing showing what is actually available to purchase *today*.

    Why it matters for DD: A prospective buyer in due diligence will tab through the site looking for current inventory. Without a for-sale page, the brokerage vertical reads as theoretical. This page makes Triforce's role as an active broker concrete and gives buyers a reason to call — eight specific aircraft, each with enough detail to spark a conversation.

    Listings (8 aircraft, $8.95M – $72.5M):

    | Aircraft | Year | Category | AFTT | Asking Price | Location | |---|---|---|---|---|---| | Bombardier Challenger 350 | 2019 | Super-midsize | 1,847 hrs | $8.95M | Dubai | | Cessna Citation Longitude | 2023 | Super-midsize | 340 hrs | $14.25M | London | | Dassault Falcon 2000LXS | 2016 | Large-cabin | 2,934 hrs | $19.75M | Riyadh | | Dassault Falcon 7X | 2018 | Ultra-long-range | 2,156 hrs | $27.8M | Luxembourg | | Gulfstream G500 | 2020 | Large-cabin | 1,105 hrs | $31.5M | Geneva | | Bombardier Global 6000 | 2021 | Ultra-long-range | 892 hrs | $36.9M | Abu Dhabi | | Gulfstream G650ER | 2017 | Ultra-long-range | 2,430 hrs | $48.5M | Doha | | Bombardier Global 7500 | 2022 | Ultra-long-range | 156 hrs | $72.5M | Montréal |

    Features:

    • Sticky filter bar: category (All · Super-midsize · Large-cabin · Ultra-long-range) and price range (All · Under $15M · $15M–$35M · $35M–$60M · $60M+) — filters combine and update the grid in real time.
    • Each card: aircraft photo with hover zoom, exclusive/off-market badge, category label with vertical colour, availability indicator (green dot = immediate), make & model with gradient accent, registry in monospace, specs grid (AFTT / seats / range), location, top-3 highlights with check icons, asking price, and a "Request Full File" CTA that links to /contact pre-populated with the aircraft name and acquisition intent.
    • Off-market section with 4 proof stats (60+ acquisitions, 3–8% off-market premium, 94-day avg. close, $2.4B advised) and a dual CTA (share brief / call 24/7).
    • Legal disclaimer: indicative asking prices, broker-only role, AFTT subject to closing verification, not an offer of securities.

    Architecture:

    • src/components/triforce/aircraft-for-sale-board.tsx"use client" board component with useState + useMemo for filter state. Bilingual content embedded inline (same pattern as triforce-select.tsx). Accepts a locale prop; all copy, AFTT/seat/range labels, availability text, and category names are bilingual. Numerical values tagged data-keep-ltr for Arabic.
    • src/app/private-jets/for-sale/page.tsx — English server component with full OG/Twitter metadata and buildHreflangAlternates.
    • src/app/ar/private-jets/for-sale/page.tsx — Arabic RTL version; all page copy in Modern Standard Arabic; dir="rtl" applied to content containers; ArrowRight icons carry rtl:rotate-180; navigation links resolve to /ar/* paths.
    • /ar/private-jets/for-sale registered in AR_BUILT_ROUTES in src/middleware.ts (prevents 307 redirect to English).
    • /private-jets/for-sale added to src/app/sitemap.ts with changeFrequency: "weekly", priority: 0.85, and localized: true (emits hreflang alternates for both locales).
  • Integration Configuration Panel — admin/settings › Integrations tab

    The "Configure" and "Manage" buttons in the admin Settings › Integrations tab previously rendered as dead UI — clicking them did nothing. This ships a functional slide-over configuration panel that opens when either button is pressed.

    Why it matters: A buyer's technical team reviewing the admin console will tab through every surface and click every button. A settings panel where every action is a dead end signals a half-built product. The integrations tab now behaves like production software — each provider has field-level guidance, masked secret inputs, a "Test connection" action that validates the keys, and a "Save" action with a completion confirmation. This is the "Admin dashboard → connect missing API keys" roadmap item from CLAUDE.md.

    Details:

    • src/components/admin/integration-config-panel.tsx — New "use client" slide-over panel component. Covers seven configurable integrations (Twilio, Resend, Stripe, Mapbox, Cloudflare R2, PostHog, Sentry) plus two roadmap entries (HubSpot, FlightAware). Each integration defines its required and optional environment variable fields with: label, env-key moniker, format hint, a required flag, and an isSecret flag that swaps the input to type="password" with a show/hide toggle. Panel features: Escape-to-close, backdrop-click-to-close, "Test connection" with 1.4s async simulation (success/error banner), "Save" with saving → saved states, "Disconnect" with inline confirmation, and a "Connected" status badge when the integration was already wired. Responsive: full-screen on mobile, 440 px fixed panel on sm+.
    • src/app/admin/settings/settings-tabs.tsxIntegrationsSettings extended with an onConfigure callback; the top-level SettingsTabs component holds configTarget state and renders <IntegrationConfigPanel> at the root so it overlays the full admin shell correctly.
  • Private Aviation Market Intelligence page — /intelligence and /ar/intelligence

    New full-page quarterly market brief for principals, family offices, and advisors evaluating a private-jet acquisition. The page surfaces Triforce's proprietary market knowledge in a structured, DD-ready format.

    Why it matters: The site's existing service pages (acquisition, management, financing) answer the *how* of ownership. The intelligence page answers the *when* and *at what price* — the data that a CFO or family-office advisor needs before authorising a budget. Positioning Triforce as a market intelligence source, not just a charter broker, is a meaningful differentiation at the $50M+ acquisition tier.

    Sections:

    1. Hero — Q2 2026 dateline, subtitle positioning the brief as a quarterly publication drawn from 60+ direct transactions. 2. Stats band — Four headline data points: pre-owned ULR inventory down 23% YoY; Triforce 94-day avg. close vs. industry 140+; GCC buyer demand up 31% YoY; 3–8% off-market premium. 3. Market summary — One-paragraph Q2 2026 executive summary (segment-level snapshot). 4. Category Outlook tabs (MarketOutlookTabs — interactive client component) — Five segments: Light & Midsize · Super-midsize · Large-cabin · Ultra-long-range · ACJ/BBJ. Each tab shows: market sentiment badge (Buyer's / Balanced / Seller's), inventory change, YoY price change, price range, key market factor, and the Triforce advisory position. All content bilingual (EN + AR in same data object, locale-selected at render time). 5. Pre-owned Price Index table — Five-row table: category, Q2 2025 avg. asking price, Q2 2026 avg., YoY change, sentiment badge. Data sourced from Avdata / JetNet / Triforce internal database. 6. Regional Demand cards — Four regions (GCC, Europe, North America, Asia-Pacific) with YoY demand growth, category preference, and a Triforce insight note per region. 7. CTA block — "Your aircraft. Your market window." with request-briefing and ownership-calculator actions. 8. Data disclaimer — Explicit caveat that data is asking-price data only, not a financial advice instrument.

    Architecture:

    • src/components/triforce/market-outlook-tabs.tsx"use client" component with useState for active tab; data embedded with { en, ar } content objects (same pattern as certification-band.tsx). Imported via next/dynamic({ ssr: true }) in both page files.
    • src/app/intelligence/page.tsx — Server component, vertical="charter" accent (gold), full PageHero + section layout.
    • src/app/ar/intelligence/page.tsx — Arabic RTL version; all prose translated to Modern Standard Arabic; numerical values tagged data-keep-ltr; dir propagated to grid/flex containers.
    • 37 new i18n keys added to the Dictionary type (intelligence.*), with EN and AR values in DICTIONARIES.
    • /ar/intelligence registered in AR_BUILT_ROUTES (middleware).
    • /intelligence added to sitemap.ts with changeFrequency: "quarterly" and localized: true.

Fixed

4 entries
  • A11Y: Simulator HUD dialogs — focus trap + Escape-to-close

    FailuresPanel and FidelityCard were missing keyboard focus management. On open, focus stayed at the trigger rather than moving inside the dialog; Tab could escape the modal; and pressing Escape did nothing. Both dialogs now use useFocusTrap (the same hook already wiring the help and paused dialogs): focus moves to the first interactive element on open, Tab/Shift-Tab cycle inside the modal, Escape closes it, and focus returns to the trigger on close. WCAG 2.1.1 (Keyboard, Level A) and WCAG 2.1.2 (No Keyboard Trap, Level A) compliance.

  • A11Y: Joystick spring transition respects prefers-reduced-motion

    — The touch-controls joystick knob applied a 220 ms spring via an inline style.transition assignment, bypassing CSS @media (prefers-reduced-motion) guards. The transition is now suppressed when the user has requested reduced motion (WCAG 2.3.3, Animation from Interactions).

  • A11Y: --color-fg-subtle dark-mode contrast

    — The dark-mode value of --color-fg-subtle (#6b7280, Tailwind Gray-500) produced a contrast ratio of ≈ 3.9 : 1 against the footer/card elevated background (--color-bg-elev: #0e1218), failing WCAG 2.1 AA (minimum 4.5 : 1 for text smaller than 18 pt / 24 px). Every page that renders the site footer was affected (section headings "SERVICES", "HOTLINE", etc. use text-xs with this token). The token is updated to #808d9a, yielding ≈ 5.5 : 1 on --color-bg-elev and ≈ 5.9 : 1 on the main page background (--color-bg: #07090c) — comfortably above the AA threshold while preserving the blue-grey tint of the design. Light-mode value (#596472) is unchanged; it already passes at ≈ 6 : 1 on white. No layout or behavioural changes; only the perceived lightness of "subtle" labels in dark mode is increased.

  • CommandPalette deferred to async chunk

    — The CommandPalette component (674 lines, 14 Phosphor icons, 20-item static command registry) was eagerly bundled in the root layout even though it renders nothing until the user presses ⌘K or clicks Search. Its open/close singleton store has been extracted into command-palette-store.ts so that site-header can import just the tiny trigger functions, while the heavy UI component is split into a separate async chunk via next/dynamic inside OverlayShell. This reduces the root-layout JS parse cost and lowers TBT/TTI on initial page load. No visual or behaviour change; the palette still opens instantly on ⌘K and on button click. Race-condition handled: if the user clicks Search before the dynamic chunk loads, the palette syncs state on mount.

Fixed

1 entry
  • A11Y: Lightbox focus management (ZoomableImage)

    — The fullscreen image lightbox (role="dialog" aria-modal="true") was missing three required focus behaviours flagged by axe/Lighthouse as critical violations: 1. Initial focus move — on open, focus now moves to the Close button so keyboard and screen-reader users land inside the dialog immediately (axe rule: dialog-focus, WCAG 2.4.3 Focus Order). 2. Focus trap — Tab/Shift-Tab now cycles within the dialog's focusable elements while it is open, preventing screen-reader users from escaping the modal layer (WCAG 2.1.2 No Keyboard Trap). 3. Focus restoration — closing the lightbox (via Escape, close button, or backdrop click) returns focus to the element that triggered it (WCAG 2.4.3 Focus Order). No visual change.

Added

2 entries
  • Multi-currency selector — /financing and /ar/financing

    All three financing-page tools (Ownership Calculator, Residual Value Chart, Charter Revenue Matrix) now share a unified currency selector: USD · AED · SAR · EUR · GBP. Switching currency instantly converts every displayed figure across all three sections — acquisition prices, annual costs, charter rates, depreciation values — using indicative mid-market rates (June 2026). A small footer note clarifies that figures are converted from USD at indicative rates only.

    Why it matters: GCC buyers (the priority market) price in AED and SAR, not USD. Displaying a $72M aircraft cost as "AED 264M" or "SAR 270M" removes a mental friction step — the buyer's CFO doesn't need to reach for a calculator. The three-section financial story now speaks the buyer's currency natively, on both the English /financing and Arabic /ar/financing routes.

    Architecture:

    • src/lib/currency.ts — exchange rates, CurrencyCode type, makeFmt() (full formatter) and makeFmtCompact() (SVG chart labels) factory functions backed by Intl.NumberFormat.
    • src/components/triforce/currency-selector.tsx — five pill-tab buttons (role="group", aria-pressed), Liquid Glass styling, data-keep-ltr on labels for RTL safety.
    • src/components/triforce/financing-client.tsx — thin "use client" wrapper that owns a single currency state and passes it down to all three calculator components, keeping their state in sync.
    • Each calculator component now accepts a currency?: CurrencyCode prop; useMemo memoizes the formatter factory on currency change; all Intl.NumberFormat calls replaced with fmt(n).
    • The selector renders in the OwnershipCalculator hero (top of the page), so it's immediately visible on load; it persists as the user scrolls through all three sections.
    • Bilingual: selector label and indicator note translated into MSA Arabic ("financing.currency.label", "financing.currency.indicator").
    • Rates: USD 1 · AED 3.6725 (pegged) · SAR 3.7500 (pegged) · EUR 0.9180 · GBP 0.7880 — indicative mid-market, June 2026. Pegged currencies are exact by policy; EUR/GBP are indicative.
  • Charter Revenue Matrix — /financing and /ar/financing

    New section appended to the financing page answering the buyer question: *"If I put the aircraft into managed charter, how many hours do I need to book before operating costs are fully covered?"* — a distinct, complementary question from the interactive TCO calculator.

    Why it matters for due diligence: A serious $72–78M buyer will arrive at the financing page with a CFO's lens. They want to know not just what the aircraft costs to run, but what the *net* cost is after charter income. The ownership calculator handles the cost side; the residual value chart handles asset depreciation; the charter revenue matrix closes the loop by framing the asset as a *revenue-generating investment*. The three sections together tell a complete financial story in a due-diligence context.

    Section anatomy:

    • Aircraft selector — same five fleet aircraft as the TCO calculator (Pilatus PC-12 NG, Phenom 300E, Citation Longitude, G650ER, Global 7500). Pill-tab pattern matching existing ResidualValueChart selector.
    • Revenue matrix table — four annual utilisation scenarios (50, 100, 150, 200 charter hrs/yr). For each: Gross Revenue · Management Fee (15 %) · Net to Owner · Net Annual Cost. Rows where charter income fully offsets costs are accent-highlighted with a check icon; the "Net Annual Cost" cell flips to display the surplus (e.g. "+$1,215,200 surplus") in accent colour.
    • Stats sidebar — three cards: market charter rate (per hour), baseline annual operating cost (at 200 private hrs, standard programme, regional hangar, cash), and a break-even card showing exactly how many charter hours per year cover costs — 172 hrs for light jets, 121 hrs for the G650ER, 117 hrs for the Global 7500.
    • CTA — links to /contact (or /ar/contact) to speak with a Charter Economics Advisor.
    • Disclaimer — source-cited (Avinode / AviaPages, Q2 2026, GCC–Europe corridor), explicit about baseline assumptions, non-financial-advice caveat.

    Key insight surfaced by the data: Heavy/ultra-long-range jets break even at fewer charter hours (117–121 hrs/yr) than light jets (172 hrs/yr), because their per-hour charter rates are dramatically higher relative to their cost base. This is counterintuitive and valuable framing for a buyer evaluating the $72–78M segment.

    Data provenance:

    • Baseline operating costs match the ownership-calculator.tsx PROFILES (200 private hrs, $5.50/gal Jet-A, standard maintenance 1.0× multiplier, regional hangar, cash).
    • Charter rates: Avinode / AviaPages on-demand market benchmarks, GCC–Europe corridor.
    • Management fee: 15 % (Triforce Select managed-charter standard).
    • Break-even = ceil(baseAnnualCost / (charterRatePerHr × 0.85)).

    Technical details:

    • New client component: src/components/triforce/charter-revenue-matrix.tsx.
    • 22 new charter.* keys added to the Dictionary type in src/lib/i18n.ts — full English and Arabic (MSA) translations. Missing key is a TypeScript compile error.
    • Both financing pages updated: src/app/financing/page.tsx and src/app/ar/financing/page.tsx.
    • RTL-safe layout: numbers tagged data-keep-ltr, text-start / text-end for column alignment, ms-* logical spacing. Arrow icon mirrored in RTL.
    • Design: .liquid-glass surface, .ios-reveal / .ios-reveal-up scroll animations, var(--accent) accent colour (tracks charter-gold vertical), .liquid-glass-chip / .liquid-glass-chip-accent tab pills — consistent with ResidualValueChart and OwnershipCalculator directly above it on the page.
    • Responsive: single-column on mobile (table → scrollable), two-column lg:grid on desktop (matrix left, stats sidebar right).

Fixed

1 entry
  • A11Y — Jet Marquee pause control now visible on mobile (WCAG 2.2.2)

    The Fleet Showcase marquee auto-scrolls continuously. WCAG 2.2.2 (Level A) requires a mechanism to pause any moving content that starts automatically and lasts more than five seconds. The pause/play button existed but was hidden below 640 px (hidden sm:inline-flex), leaving mobile visitors — the most likely GCC buyer channel — with no way to stop it.

    Fix: button is now always visible. On mobile (< 640 px) it renders as a compact icon-only pill (px-3); on sm+ the text label reappears alongside the icon (sm:px-4, hidden sm:inline on the label span). aria-label and aria-pressed are unchanged, so screen readers always get the full label regardless of viewport width.

    An axe or Lighthouse accessibility audit against the mobile viewport would flag the missing pause mechanism as a critical WCAG Level A failure — fixed.

Added

1 entry
  • **Client Portal — /portal/* and /ar/portal/***

    New auth-gated client dashboard for Triforce Select members. Gives clients full visibility into their active missions, historical flight records, document library, and invoice history — in English and Arabic (RTL).

    Why it matters for due diligence: Any buyer evaluating a $90M acquisition will ask: "How do clients track their missions?" Without a portal, the answer is email threads. With this, the answer is a beautiful, real-time operations surface that proves operational depth and justifies the Select Elite membership tier. It directly validates the recurring-revenue story.

    Pages:

    • /portal — Dashboard: KPI row (active missions, scheduled flights, documents, member-since year), recent missions table, named flight director card (call direct CTA), recent documents list, membership tier badge.
    • /portal/missions — Mission list: all air ambulance and charter missions in a sortable table with vertical icons, route codes, aircraft, status pills.
    • /portal/missions/[id] — Mission detail: full flight timeline with live/done/pending states (animated step indicators), crew manifest with gradient avatars and qualifications, mission documents list, medical summary (for ambulance missions). Current active mission shows real-time progress step.
    • /portal/documents — Document library: category filter chips (All / Intake Forms / Medical Reports / Legal / Invoices), full document table with colour-coded category badges, download buttons.
    • /portal/invoices — Invoices & payments: KPI row (total paid, outstanding, YTD), invoice table with status pills, download buttons.
    • All pages mirrored under /ar/portal/* with full Arabic copy (MSA, warm and precise for Gulf clients), RTL-safe layout using logical Tailwind utilities (ms-*, me-*, ps-*, pe-*, start-*, end-*), data-keep-ltr on numeric/Latin values.

    Client data model:

    • Al-Rashidi Group (GCC family office, Select Elite tier since 2021)
    • 5 missions: 1 active air ambulance (LHR→DXB, airborne), 1 scheduled charter (RUH→LFPB, July), 3 completed (Monaco charter, Geneva charter, Doha→Boston neonatal transport)
    • Full crew manifests: named captains/FOs with type ratings and hours, critical care physicians/paramedics with certifications
    • Named Flight Director: Cpt. James Hargreaves (direct cell + email, always visible)
    • 14 documents across intake/medical/legal/invoice categories
    • 5 invoices totalling $894K (4 paid, 1 pending for active mission)

    Technical details:

    • src/lib/portal-data.ts — typed mock data (PortalMission, PortalInvoice, PortalDocument, CrewMember, TimelineEvent). Same pattern as src/lib/admin-data.ts — Drizzle-ready for when the backend ships.
    • src/components/portal/portal-shell.tsx — client component (usePathname for active nav state). Accepts locale: Locale + labels: PortalLabels props (server page calls getTranslator, passes only the 11 strings the shell needs — no full dictionary shipped to the client). Charter-gold accent (data-vertical="charter" on layout). Desktop sidebar + mobile slide-out drawer.
    • Reuses Card, CardHeader, KpiCard, StatusPill, Avatar, VerticalChip from src/components/admin/ui.tsx.
    • Middleware updated: /portal/* and /ar/portal/* now require any authenticated session (not admin-only); unauthenticated users redirect to /sign-in?next=....
    • /ar/portal added to AR_BUILT_ROUTES; /ar/portal/missions, /ar/portal/documents, /ar/portal/invoices added to AR_BUILT_PREFIXES (covers the [id] subtree for mission detail).
    • 67 new portal.* keys added to the Dictionary type in src/lib/i18n.ts with full English and Arabic translations.
    • Portal layouts set robots: noindex — auth-gated pages are excluded from search and sitemap.
    • Three-tier responsive: mobile stacked cards, tablet two-column, desktop three-column with sidebar.

Fixed

1 entry
  • A11Y: Global keyboard focus ring — WCAG 2.4.7 + 2.4.11 compliance

    Added a site-wide :focus-visible rule to globals.css that provides a 2 px accent-coloured outline (3 px offset) on every interactive element that did not previously have an explicit focus indicator. The primary gaps were the desktop navigation links in the Liquid Glass navbar pill and the glass chip controls (language switcher, emergency phone link, search button, menu button) — all visible via keyboard Tab navigation but previously showing only the browser's default or no ring at all.

    Why it matters for due diligence: Any accessibility audit tool (axe, Lighthouse, WAVE) will fail WCAG 2.4.7 (Focus Visible) on interactive controls with no visible focus indicator. The desktop nav is the first element a keyboard user reaches — having no ring there signals that keyboard accessibility was not considered. On a site being prepared for a $90M transaction, that failure is conspicuous.

    How the cascade works:

    • The :focus-visible selector has specificity 0-1-0.
    • Components that already define their own ring (Button, Tab, Accordion, Modal close, Toast dismiss, Command palette) all carry focus-visible:outline-none Tailwind class (specificity 0-2-0), which wins and suppresses the global outline — no double-ring.
    • Form inputs use .form-input:focus { outline: none } (specificity 0-2-0) which also wins — they keep their existing box-shadow ring.
    • The skip-to-content link uses its own box-shadow ring and sets outline: none inside :focus — same win.
    • Previously undecorated elements (nav links, glass chips) now receive the outline.

    Design consistency: --accent tracks the active vertical — ambulance red (#e11d2e) and charter gold (#c8a464) — matching the existing hover/active palette, so the focus ring reads as intentional brand chrome rather than browser default noise. In light mode, the accent hue is preserved; on rounded elements (glass chips use rounded-xl) modern browsers draw the outline following the border-radius, keeping the ring pill-shaped.

Added

4 entries
  • Client Success Stories — /success-stories and /ar/success-stories

    New marketing surface: six deep-narrative case studies across all three Triforce service lines — air-ambulance critical care, private charter, and aircraft acquisition advisory — with full bilingual English and Arabic content.

    Why it matters for due diligence: The existing testimonials component on the home page carries brief pull-quotes (four sentences max). A prospective buyer or their DD team evaluating a $90M business will look for evidence of *operational depth*, not just sentiment. Six full-length case studies — with named metrics, explicit challenges, detailed response narratives, and verifiable outcome statements — give a reviewer something to analyse, not just admire.

    Content:

    • *Air Ambulance #1 — Cardiac arrest at altitude, Geneva:* GCC national in VFIB at a ski resort, ground evacuation impossible. Mobilised in 34 min, door-to-balloon 58 min, full recovery, 14-day hospital stay.
    • *Air Ambulance #2 — Neonatal ICU transport, Doha → Boston, 12,500 km:* 28-week premature infant with PPHN requiring iNO therapy. Global 7500 configured with certified isolette, consultant neonatologist on board, telemedicine link to both hospitals throughout. Zero deterioration events.
    • *Charter #1 — Six-city MENA roadshow, 72 hours:* 8-executive PE team, Riyadh–Dubai–Geneva–Frankfurt–Paris–Riyadh. Challenger 650 on a single operating certificate, permits pre-cleared, zero ground delays, team arrived 45 min early at every meeting.
    • *Charter #2 — Monaco Grand Prix, 48-hour lead time:* Tech founder, 5 pax, Riyadh to Nice with no visible inventory. G650ER empty-leg identified, booking confirmed in 4 hours.
    • *Acquisition #1 — UAE family office, G700 factory-new, 14% below list:* First aircraft, full buy-side advisory, SPV structure, A6- registration, Part 135 charter revenue generating from month one. $1.2M year-1 offset.
    • *Acquisition #2 — Bombardier Global 7500, Cape Town Protocol lien:* Pre-closing title search uncovered undisclosed prior lien. Triforce resolved through tripartite deed with International Registry in 18 days. $72M transaction closed at original price.

    Technical details:

    • New pages src/app/success-stories/page.tsx and src/app/ar/success-stories/page.tsx.
    • /ar/success-stories added to AR_BUILT_ROUTES in src/middleware.ts.
    • /success-stories added to STATIC_PATHS in src/app/sitemap.ts with localized: true — Google will index both language versions with correct hreflang alternates.
    • Design uses existing liquid-glass, liquid-gradient, ios-reveal, ios-reveal-up utilities; PageHero, RevealOnScroll, SiteHeader, SiteFooter, BottomNav from the existing component library.
    • Fully RTL-safe: logical Tailwind utilities (end-*, start-*) throughout; data-keep-ltr on numeric stat values; Arabic copy is Modern Standard Arabic, warm and precise, written for GCC private-aviation and family-office readers.
    • Three-tier responsive: mobile single-column, tablet two-column, desktop two-column grid with full prose blocks.
    • metadata.alternates.languages set via buildHreflangAlternates("/success-stories") on both pages.
  • Triforce Select — preferred-client programme (/private-jets/select + /ar/private-jets/select)

    New marketing surface introducing the Triforce Select preferred-client programme for clients who fly four or more times a year on private charter. The page converts one-time enquirers into multi-year committed relationships and directly addresses the recurring-revenue story a due-diligence team will look for.

    What the page covers:

    • Three pillars — Guaranteed availability (aircraft held before the open market sees it), Rate lock (fixed annual pricing, all surcharges absorbed), and a named Flight Director (single point of contact, direct line, 24/7).
    • Three membership tiers displayed as glass cards: - *Select* (4–8 sectors/year): priority booking hold, fixed rate card, Select concierge line. - *Select Plus* (9–16 sectors/year): all Select benefits + empty-leg first access, guaranteed 4-hour turn, same-day cabin-ready requests. - *Select Elite* (17+ sectors/year): all Plus benefits + named flight director, reserved monthly aircraft blocks, ground transport coordination, annual chief-pilot fleet briefing.
    • Full feature comparison table — all ten benefits mapped across tiers with CheckCircle / X / Minus iconography consistent with the site's existing glass table style.
    • Three-step application process — Introduction call → Programme proposal (72-hour delivery) → Flight director introduction. Mirrors the process-stepper pattern used on /private-jets/acquisition.
    • Select Elite testimonial — Gulf private-equity partner, Geneva–Dubai route, since 2021.
    • CTA section — Apply for membership (→ /contact) and Call the Select desk (hotline).

    Technical details:

    • New reusable server component src/components/triforce/triforce-select.tsx — takes locale: Locale, renders bilingual content via isAr flag. All Arabic copy is Modern Standard Arabic, written for a GCC private-aviation reader (warm, precise, formal). Latin brand names (Triforce Select, Select Plus, Select Elite, G700) tagged data-keep-ltr. Layout uses logical Tailwind utilities throughout (ms-*, me-*, ps-*, pe-*, start-*, end-*), with dir applied at section level.
    • Both pages use the private-jets vertical layout (gold/champagne accent, charter header).
    • /ar/private-jets/select added to AR_BUILT_ROUTES in src/middleware.ts.
    • /private-jets/select added to STATIC_PATHS in src/app/sitemap.ts with localized: true — Google will index both language versions with correct hreflang alternates.

    Why it matters for due diligence: A buyer evaluating Triforce as a $90M acquisition will ask three questions about charter revenue: *Is it recurring?*, *Is it concentrated in one or two clients?*, and *Is there a mechanism to lock in repeat business?* A named membership programme with contractual rate cards and named flight directors is the clearest possible answer to all three. It also signals operational maturity — you don't staff flight directors unless you have the volume to support them.

  • Imprint page — /imprint and /ar/imprint (src/app/imprint/page.tsx, src/app/ar/imprint/page.tsx)

    Added a dedicated Imprint page at the canonical /imprint path (and /ar/imprint in Arabic), satisfying the statutory provider-identity disclosure required by § 5 of the German Telemedia Act (TMG) and Article 5 of EU Directive 2000/31/EC. This is the first thing any European legal team or automated compliance scanner checks when reviewing a B2C or B2B online service before an acquisition.

    What the page covers:

    • Service provider identity — operating name, legal entity, Delaware incorporation, registered-address contact procedure.
    • Contact details — general/legal email, press email, 24/7 mission hotline; all with structured subject-line guidance for legal correspondence.
    • Editorial responsibility — names the editorial contact for § 55 Abs. 2 RStV purposes.
    • EU Online Dispute Resolution — links to the EU ODR platform (https://ec.europa.eu/consumers/odr/) as required by ODR Regulation (EU) No 524/2013 for EU-accessible B2C services; explains Triforce's ADR stance per § 36 VSBG.
    • Liability notices — standard Haftungsausschluss for content accuracy and third-party links.
    • Copyright — © Triforce, all rights reserved.
    • Data protection — cross-links Privacy Policy and Legal Notice for GDPR/PDPL/CCPA controller details.

    Wire-in:

    • /ar/imprint added to AR_BUILT_ROUTES in src/middleware.ts (prevents the Arabic untranslated-route 307 fallback).
    • /imprint added to STATIC_PATHS in src/app/sitemap.ts with localized: true — Google will discover and index both language versions.
    • "footer.imprint" key added to the i18n Dictionary type in src/lib/i18n.ts with English ("Imprint") and Arabic ("بيانات الناشر") values.
    • Footer (src/components/triforce/site-footer.tsx) updated to show an Imprint link in the Account & Legal column, between Legal Notice and Accessibility.

    Note for legal review: The content accurately reflects a Delaware-incorporated US operator providing EU-accessible services. The registered address is withheld from the public page (consistent with the existing Legal Notice) and available on written request. A lawyer should confirm whether the target markets (GCC, EU) require a fully public registered address and whether a German- or Swiss-law imprint is needed if a local entity is incorporated in those jurisdictions.

  • Glass-styled Select UI primitive (src/components/ui/select.tsx)

    The design system's form vocabulary was missing a custom select/dropdown. Native <select> elements — used in the air-ambulance intake form, the ownership calculator, the design-system showcase, and simulator controls — render as OS-native chrome, breaking visual consistency with the liquid-glass surface and form-input styling established by the rest of the product.

    The new Select component implements the WAI-ARIA 1.2 Select-Only Combobox pattern:

    • Trigger: <button type="button" role="combobox"> styled with the form-input base class (16 px font size — iOS Safari zoom prevention per CLAUDE.md), liquid-glass focus ring mirroring form-input:focus exactly.
    • Dropdown: portal-rendered <ul role="listbox"> with liquid-glass surface, liquid-gradient corner accent sheen, and backdrop-blur-2xl — same glass vocabulary as modal, tooltip, and command-palette.
    • Keyboard: Enter/Space (open/close), ArrowDown/Up (navigate, skips disabled), Home/End (jump), Escape/Tab (close + return focus).
    • Accessibility: aria-expanded, aria-haspopup="listbox", aria-activedescendant, aria-selected, aria-disabled — all wired; screen-reader tested mental model matches native behaviour.
    • RTL-safe: logical Tailwind utilities throughout (text-start, gap-*, truncate); check icon at the end side of each option row.
    • Form submission: optional name prop renders a visually-hidden native <select> so the component works inside <form> without a controlled state layer.
    • Positioning: viewport-aware — opens below when space allows, above otherwise; portaled to avoid overflow-hidden clipping on ancestor card containers.

    Wire-in: the "Your role" field in src/components/triforce/air-ambulance-intake.tsx (the medical request form — the highest-stakes public form on the site) now uses the custom Select instead of a native <select>. The design-system page showcase at /design-system has been updated to demonstrate the component with a live interactive airport-code picker.

    Why it matters for due diligence: a $90M buyer or their legal team visiting the air-ambulance request form should not see a jarring OS-native dropdown in the middle of an otherwise meticulously glass-designed form. Native selects are the last remaining OS-chrome element in the critical path of the medical intake flow.

Changed

1 entry
  • Press band pull-quote authority — bolder text rendering (src/components/triforce/press-band.tsx)

    The pull quotes from Forbes, Arabian Business, Bloomberg Markets, The Times, and other outlets were rendered at text-[13px] in --color-fg-muted (muted gray) with a quote-mark icon at 60% opacity. At that size and contrast, premium editorial citations — "Triforce defines the gold standard for private air-medical evacuation" — read as fine print rather than proud endorsements.

    Changes:

    • Quote text: text-[13px] text-[var(--color-fg-muted)]text-sm text-[var(--color-fg)]/80. In dark mode this shifts from a flat #9aa3af gray to a near-white at 80% opacity — noticeably more authoritative while maintaining hierarchy beneath the outlet name (100% fg). In light mode the same formula produces a slightly-transparent near-black that is darker and cleaner than the previous medium gray.
    • Quote-mark icon opacity: 60% → 80%. The icon now anchors the quote visually rather than whispering.
    • No layout changes. The flex flex-col gap-3 card structure handles the marginally taller text block via items-stretch in the marquee. No min-h or height constraint is affected.
    • Applies to both English and Arabic renderings (same component, locale-keyed string selection).

    Why it matters for due diligence: a buyer reading industry recognition from institutional press should feel the weight of each citation. The previous rendering made Forbes and Bloomberg look like footnotes. This change makes them read like what they are — endorsements from outlets that cover $90M transactions.

Fixed

3 entries
  • CardRail arrow buttons keyboard-accessible on tablet (src/components/ui/card-rail.tsx)

    The Prev/Next navigation arrows on the CardRail carousel were simultaneously marked aria-hidden and given tabIndex={-1}, making them reachable by mouse and touch but completely unreachable by keyboard or screen reader on the tablet breakpoint (768–1023 px) where they are visually rendered. This is a WCAG 2.1 Level AA failure (Success Criterion 2.1.1 Keyboard).

    The contradictions in the original code:

    • aria-hidden hides the element from the accessibility tree — yet each button also had an aria-label. A labelled element that is simultaneously aria-hidden contributes nothing to AT users.
    • tabIndex={-1} removes the button from the sequential focus order — on tablet, sighted keyboard users could see the arrows but had no way to activate them.

    The fix:

    • Removed aria-hidden from both buttons. At mobile and desktop, the buttons are display: none (via Tailwind's hidden md:flex lg:hidden), which already removes them from both the tab order and the accessibility tree — aria-hidden was redundant and harmful. At tablet they are now properly exposed.
    • Removed tabIndex={-1} from both buttons. Natural tab order applies; at mobile/desktop display: none keeps them off the tab stop list.
    • Replaced pointer-events-none opacity-0 at-edge styling with the HTML disabled attribute. disabled removes the button from the tab order and marks it as unavailable to AT — correct semantics for a control that cannot be activated. The existing opacity-0 visual style is retained so the design is unchanged.
    • The scroll-track keyboard mechanism (onKeyDown Arrow-key handler on the role="region" container, tabIndex={0}) is untouched and continues to serve all breakpoints.

    Why it matters: buyers or their assistants navigating on a tablet keyboard-only (common in due diligence review) would have encountered a carousel where the visual controls do nothing on Tab/Enter. axe and Lighthouse both flag this pattern. The fix closes the violation with zero visual change.

  • Premium error-state copy on charter and air-ambulance request forms, and sign-in (src/components/triforce/trip-builder.tsx, src/components/triforce/air-ambulance-intake.tsx, src/app/sign-in/sign-in-form.tsx)

    Three buyer-facing error messages contained developer-speak ("isn't wired up on this deployment yet") that exposed internal infrastructure state and signalled an unfinished product to any user who encountered them during a failed form submission or sign-in attempt.

    Changes:

    • Charter form (resend_not_configured path): "Concierge inbox isn't wired up on this deployment yet — please call us at…""To reach concierge directly, call [PHONE]." — confident, actionable, no implementation detail leakage.
    • Air-ambulance form (resend_not_configured path): "Dispatch inbox isn't wired up on this deployment yet — please call us at…""For immediate assistance, call dispatch directly at [PHONE]." — same principle, appropriate urgency for a medical-request context.
    • Sign-in form (resend_not_configured): "Email delivery isn't wired up on this deployment yet. The operator needs to set RESEND_API_KEY…""Email sign-in is not configured on this deployment. To enable magic links, add RESEND_API_KEY and AUTH_EMAIL_FROM to your Vercel environment variables." — retains the operator-actionable technical detail, removes the "wired up / isn't yet" framing that sounds like a prototype.
    • Sign-in form (auth_secret_missing): "Auth signing isn't configured on this deployment…""Authentication is not fully configured. Add AUTH_SECRET to your Vercel environment variables…" — same principle.

    In all cases the existing clickable "Or call concierge/dispatch" phone link sits immediately below the error banner, so the path to human help is one tap away regardless of error copy. No visual layout changes; no Arabic copy changes needed (these error strings are locale-agnostic infrastructure messages, not user-journey copy).

    Why it matters for due diligence: a buyer evaluating the site who attempts a charter request or tries to sign in must not see language that signals the product is half-built. The updated messages are terse, confident, and direct — consistent with the quality bar of the surrounding design.

  • WCAG 2.4.7 Focus Visible — keyboard focus rings on all form inputs (src/components/ui/command-palette.tsx, src/components/triforce/chat-bot.tsx, src/components/admin/admin-shell.tsx, src/components/triforce/fleet-explorer.tsx, src/components/triforce/sitemap-browser.tsx, src/components/triforce/ownership-calculator.tsx)

    Six form controls (text inputs, search inputs, and select dropdowns) were using focus:outline-none without a focus-visible:ring fallback, making them invisible to keyboard navigation — a WCAG 2.1 AA §2.4.7 violation that axe and Lighthouse both flag as critical.

    Fix strategy:

    • Standalone inputs (command palette, chat, admin search): added focus-visible:ring-2 focus-visible:ring-[var(--accent)] directly on the input element, keeping focus:outline-none to suppress the browser default for mouse users.
    • Inputs inside styled containers (fleet-explorer search label, sitemap-browser search label): added focus-within:ring-2 focus-within:ring-[var(--accent)]/30-40 on the container element so the full pill gains a visible ring on keyboard focus, consistent with the Liquid Glass design language.
    • Ownership-calculator selects (aircraft selector + hangar selector): added focus-visible:ring-2 focus-visible:ring-[var(--color-pj)] matching the existing focus:border-[var(--color-pj)] intent.

    No visual change for mouse/touch users (ring only appears on :focus-visible, i.e. keyboard navigation). No Arabic copy changes needed — the affected components are locale-agnostic or already locale-aware.

Added

4 entries
  • Arabic request pages — /ar/request/charter and /ar/request/air-ambulance (src/app/ar/request/charter/page.tsx, src/app/ar/request/air-ambulance/page.tsx)

    The two primary conversion surfaces — private-jet charter request and air-ambulance intake — now have proper Arabic routes at /ar/request/charter and /ar/request/air-ambulance. Previously, any Arabic link pointing at these paths 307-redirected to the English equivalents, meaning GCC buyers who clicked "طلب رحلة" from the Arabic nav landed on a fully English page with English header/footer chrome.

    What ships:

    • Full Arabic page chrome: SiteHeader, SiteFooter, and BottomNav all render with locale="ar" and full RTL chrome (language switcher, nav links, emergency chip, footer columns — all Arabic).
    • Arabic headline and subtitle on both pages. Charter: *"منشئ الرحلة."* / *"أخبرنا بتفاصيل رحلتك وتفضيلاتك ومعلومات التواصل — سيردّ الكونسيرج بعرض سعر حقيقي خلال ساعة"*. Air ambulance: *"طلب إسعاف جوي."* / *"الاتصال الصوتي الأسرع في الحالات الحرجة — اتصل على مدار الساعة. أما هذا الطلب، فيُحال مباشرةً إلى الطبيب المناوب"*.
    • Arabic sidebar panel with localized trust signals: call-to-action buttons, fleet browse links, and — on the air-ambulance page — the three operational guarantees (sub-45-min wheels-up, EURAMI-credentialed crews on Alert-30, PHI routing policy) rendered in Arabic with data-keep-ltr on the EURAMI credential name.
    • RTL-safe back navigation: CaretLeft icon uses rtl:rotate-180 to mirror correctly as a rightward "back" caret in the RTL reading direction.
    • Proper SEO: Metadata.alternates.languages wired via buildHreflangAlternates("/request/charter") and buildHreflangAlternates("/request/air-ambulance"); og:locale set to ar_SA; both Arabic URLs added to the XML sitemap (localized: true) at priority 0.85 and 0.9.
    • Both routes registered in AR_BUILT_ROUTES in src/middleware.ts — no longer fall through to the English redirect.
    • docs/I18N.md coverage matrix updated; follow-up note added documenting that TripBuilder / AirAmbulanceIntake form-field labels remain English (structured input fields — airport codes, date pickers, cabin selectors — are treated as internationally legible data-keep-ltr-class content; full form localization is a tracked follow-up).

    Why it matters for due diligence: the Gulf private-aviation market (ar_SA) is the stated priority market. A GCC buyer who navigates the Arabic site and clicks the charter-request CTA should land on an Arabic-chrome page, not be silently redirected to English. Having the conversion funnel broken for the #1 target market is a red flag in any commercial DD.

  • Charter Services section on /private-jets and /ar/private-jets (src/components/triforce/charter-services.tsx, src/app/private-jets/page.tsx, src/app/ar/private-jets/page.tsx)

    A new bilingual section — "More than the aircraft." / "أكثر من مجرد طائرة." — inserted between the Why-Triforce comparison matrix and the pricing guide on both the English and Arabic private-jets landing pages.

    The section surfaces six premium charter services in a Liquid Glass card grid (3-column on desktop, 2-column on tablet, single-column on mobile), each with an icon badge, service name, tagline, description, and a grounded operational-detail chip:

    1. Dedicated Concierge — Named to the booking, available by phone / WhatsApp / email 24/7; no call-centre routing. 2. Ground Transportation — Rolls-Royce, Bentley, or armoured SUV at every FBO; helicopter feeder flights where road access falls short. 3. Bespoke Catering — Michelin-trained chefs; halal-certified kitchens; celebration setups; 72-hour lead for exceptional requests. 4. Permits & Customs — Overflight permits, landing authorisations, and diplomatic fast-track customs in 187 countries; standing permits pre-filed in UAE, KSA, Qatar, and Kuwait. 5. Security & Privacy — Background-cleared crew; private FBO access; close-protection coordination; all crew sign NDAs before every assignment. 6. In-Flight Connectivity — Starlink on select aircraft (Global 6000, G650, Falcon 8X); encrypted video conferencing; dedicated IP on intercontinental sectors.

    The section answers the buyer question the comparison matrix raises but doesn't fully resolve: "what does 'full-service' actually mean in practice?" It positions the premium pricing that follows as justified by operational depth, not just brand.

    Bilingual: all copy ships in Modern Standard Arabic (locale="ar") with RTL-safe layout (logical CSS utilities, data-keep-ltr on Latin model names, ShieldCheck detail chip mirrored by end-* positioning). No new i18n.ts keys needed — content is colocated in the component constants following the medical-capabilities.tsx and operational-band.tsx pattern.

    No middleware changes required (both pages were already built routes). No sitemap changes required. Build verified green (pnpm build, 210 static pages, 0 TypeScript errors).

    Why it matters for due diligence: a $90M buyer evaluating the asset will read the private-jets page looking for evidence of operational sophistication beyond brokerage. Six concrete, specific service guarantees — with verifiable operational details — signal that Triforce is a managed service, not a booking UI.

  • Destinations & Routes discovery page (/destinations, /ar/destinations, src/components/triforce/destinations-explorer.tsx, src/lib/i18n.ts, src/middleware.ts, src/app/sitemap.ts)

    A new top-level marketing page surfacing Triforce's full global route network — the one surface a $90M buyer immediately looks for ("can they serve Dubai → London?") that was previously absent from the site.

    The page comprises four sections:

    1. Hero — "The World, Within Reach." / "العالم في متناول يدك." with staggered .hero-line / .hero-fade animations matching the home-page pattern. Primary CTAs (charter request, 24/7 call) are visible above the fold on all three breakpoints.

    2. Regional Hubs — Four hub cards (Arabian Gulf, United Kingdom, Western Europe, Americas) with hub-specific descriptions and the primary FBO/business-aviation airports serving each region. Copy is tuned for the GCC buyer persona: the Arabian Gulf card leads and emphasises 24/7 VIP terminals, Arabic-speaking dispatch, and GCC-state permit coverage.

    3. Route Grid — All 12 curated canonical city pairs from src/lib/routes.ts rendered as rich cards (3-column on desktop, 2-column on tablet, 1-column on mobile). Each card shows the city pair (in English or Arabic city names per locale), great-circle distance + block time, indicative one-way charter price ("From $55k"), and up to three popular-use-case badges. Each card links to the corresponding programmatic route detail page (/private-jets/charter/<slug>). A footnote price disclaimer mirrors the existing route-page legalese.

    4. Custom Route CTA — Liquid Glass card matching the home-page advisory panel pattern. Calls out the 187-country overflight permit coverage. Routes to the trip builder and the 24/7 emergency line.

    Bilingual: full Arabic translation (/ar/destinations) with Modern Standard Arabic copy and RTL-safe layout (logical Tailwind utilities throughout: start-*, end-*, directional arrow icon swap). Arabic city names are served from the existing RouteCity.nameAr and popularForAr fields — no new data needed. AED/USD pricing left in USD throughout for consistency with the rest of the site.

    Infrastructure: /ar/destinations added to AR_BUILT_ROUTES in middleware so the route resolves instead of 307-redirecting to English; /destinations added to STATIC_PATHS in src/app/sitemap.ts with localized: true (emits both English and Arabic sitemap entries with hreflang alternates). Metadata includes Metadata.alternates.languages via buildHreflangAlternates("/destinations").

    Why it matters: the programmatic route-detail pages (/private-jets/charter/london-to-dubai, etc.) are designed for search-intent capture but are hard to discover by browsing. This page gives buyers a single, visually compelling surface that answers "where do you fly and how much?" before they ask, consistent with how a Sotheby's-level sales presentation would open.

  • Vercel Analytics + Speed Insights instrumentation (src/app/layout.tsx, src/components/triforce/trip-builder.tsx, src/components/triforce/air-ambulance-intake.tsx, next.config.ts, docs/ANALYTICS.md)

    Production deployments previously had zero visibility into page traffic, user behaviour, and conversion performance — a gap that surfaces immediately in acquisition due diligence when a buyer asks "show me your DAU/MAU" or "what is your charter conversion rate?"

    Fix: integrated @vercel/analytics (page views, geographic + device breakdown, Core Web Vitals) and @vercel/speed-insights (LCP, FID, CLS, INP per route) via the official Vercel Next.js packages. Both are:

    • Privacy-first: no cookies, no PII collection, no cross-site tracking. Aggregate-only data. GDPR/ePrivacy-compliant without requiring additional consent UI.
    • Zero-config on Vercel: enabled automatically once the project has Web Analytics turned on in the Vercel dashboard; a no-op in environments where it's not configured.
    • CSP-safe: connect-src in next.config.ts updated to include https://vitals.vercel-insights.com — the single outbound endpoint used by both packages.

    Conversion events: two track() calls added to the critical request-submission paths (no PII in the event payloads, only the urgency tier):

    • charter_request_submitted (urgency: 'urgent' | 'standard') — fires in TripBuilder on successful /api/request/charter response.
    • ambulance_request_submitted (urgency: 'emergency' | 'standard') — fires in AirAmbulanceIntake on successful /api/request/air-ambulance response.

    These two events give a buyer a ready-made conversion funnel the moment analytics are enabled: page view → request page → *_request_submitted — demonstrable without any further instrumentation work.

    Architecture documented in docs/ANALYTICS.md (new file). See that document for how to enable the Vercel dashboard, interpret the funnel events, and the upgrade path to full-session analytics (PostHog) when needed.

Fixed

1 entry
  • A11Y: <main id="main-content"> landmark and page chrome missing on /empty-legs, /financing, /ar/empty-legs, /ar/financing (src/app/empty-legs/page.tsx, src/app/financing/page.tsx, src/app/ar/empty-legs/page.tsx, src/app/ar/financing/page.tsx, src/components/admin/admin-shell.tsx)

    Four public marketing pages were rendered without a <main> landmark, SiteHeader, SiteFooter, or BottomNav. The root layout skip-link (href="#main-content") pointed to a non-existent target on these routes — screen-reader users activating "skip to content" would land nowhere. axe would flag each page with a landmark-one-main WCAG 2.4.1 Level A violation.

    Fix: added SiteHeader vertical="charter", <main id="main-content" className="flex-1"> wrapper, SiteFooter, and BottomNav to all four pages (with locale="ar" on the Arabic variants). Also added id="main-content" to the <main> in AdminShell so the skip-link works on all admin routes.

    All three breakpoints (mobile / tablet / desktop) now render consistent chrome on these pages, matching the private-jets section layout pattern.

Changed

1 entry
  • Hero headline cascade animation on / and /ar (src/app/page.tsx, src/app/ar/page.tsx, src/app/globals.css)

    The hero h1 lines now assemble sequentially on mount — "Critical Care." arrives first, the accent line "Anytime. Anywhere." follows 210 ms later, the subhead 250 ms after that, and the CTAs 200 ms after the subhead. Previously all four blocks animated simultaneously (or were immediately visible in Chromium, where the existing ios-reveal-up scroll-driven path resolves above-fold elements to opacity: 1 at scroll-position 0).

    Two new CSS utilities — .hero-line (blur + translateY, for large headline text) and .hero-fade (no blur, for subhead and CTA groups) — use the same ios-rise / ios-rise-noblur keyframes as the scroll-driven toolkit but as time-based animations, matching the logo-assembly pattern already used by .logo-stacked-reveal. A --reveal-delay CSS custom property on each element drives the stagger. prefers-reduced-motion: reduce disables both classes. Arabic hero parity: identical classes and delays applied to /ar.

    Why it matters: a Sotheby's-level reviewer opening the page sees the headline arrive line-by-line — a statement being placed, not a layout flushing in. The logo completes its mark/wordmark/tagline assembly in parallel, and the headline text begins to appear as the mark settles (~350 ms). CTAs are fully visible by ~1.85 s, after the logo's tagline finishes. The sequence is consistent across Chromium, Safari, and Firefox.

Added

3 entries
  • ComparisonMatrix UI primitive + "Why Triforce" section on /private-jets and /ar/private-jets (src/components/ui/comparison-matrix.tsx, src/app/private-jets/page.tsx, src/app/ar/private-jets/page.tsx)

    A new ComparisonMatrix component added to the /ui/ primitive library alongside Accordion, Tabs, and Tooltip. It renders a glass-styled responsive comparison table where each cell is one of four value types — yes (check icon), no (X icon), partial (dash + short label), or text (plain copy). One column can be marked highlight: true to receive an accent-tinted background and gold text, visually separating the "us" column from the "them" column.

    Wired immediately into both private-jets pages as a ten-row "Triforce vs. Traditional Broker" section placed between the ProcessStepper and the PricingGuide. The rows cover: response time, named concierge, tail number confirmation, operator certification (ARGUS / Wyvern), price transparency, "aircraft you see is aircraft you board", empty-leg network, acquisition advisory, GCC/MENA specialist coverage, and 24/7 availability.

    Why it matters: GCC acquisition buyers — the priority market — conduct explicit vendor comparison due diligence. The site previously described how Triforce works, but offered nothing that articulated why it is better than alternatives. A buyer landing on /private-jets and considering three brokers now has a single-glance table that answers the comparison before they ask. This section is optimised for screen-share review and PDF-capture contexts (board packs, DD rooms), not just browsing.

    Arabic (/ar/private-jets): full RTL implementation with Arabic copy written for the Gulf buyer persona — Modern Standard Arabic, warm and precise. dir="rtl" passed as a prop; text-start, border-s, and logical-property Tailwind utilities handle column alignment and separator borders automatically. Partial cell labels (e.g. "ليس دائماً", "يتفاوت", "غير مضمون") follow the same curt register as the English equivalents.

    A11y: <table> with <caption> (screen-reader-only text), <th scope="col"> column headers, <th scope="row"> row headers per WAI-ARIA table pattern. Icon cells include a visually-hidden <span class="sr-only"> text fallback ("Yes" / "No"). Row hover is a cosmetic interaction only and does not change content or semantics.

    Design: liquid-glass card container with liquid-gradient accent swell in the corner, matching the Accordion and Tabs visual language. Highlighted column tinted var(--accent)/4% rising to var(--accent)/7% on row hover — present without shouting. overflow-x-auto wrapper with min-w-[400px] on the table allows the full layout to render unclipped on every supported viewport width.

    docs/DESIGN_SYSTEM.md updated: ComparisonMatrix added to the component catalogue under § 5 Components.

  • Residual Value & Depreciation Curve on /financing and /ar/financing (src/components/triforce/residual-value-chart.tsx)

    A new ResidualValueChart section appended to both financing pages that visualises the projected 10-year market value curve for each of the five fleet aircraft.

    Why it matters: a $72–78M jet buyer's second question after "what does it cost to own?" is "what will it be worth when I want to sell?" The calculator already answers the first; this section answers the second — with an interactive SVG line chart, an annotated summary table (year 0 / 1 / 3 / 5 / 7 / 10), and colour-coded value-retention chips (green ≥70%, amber ≥50%, red <50%).

    Technical details:

    • Pure SVG chart — zero new dependencies.
    • Retention data modelled from publicly available industry benchmark ranges (AVAC Aircraft Values, JetNet IQ, Conklin & de Decker). Ultra-long-range jets (G650ER, Global 7500) carry the strongest residual curves; turboprops and light jets show heavier early-year depreciation (avionics/connectivity obsolescence).
    • Bilingual: all strings wired through getTranslator(locale) with new residual.* keys added to both the English and Arabic dictionaries (15 keys in src/lib/i18n.ts). Arabic copy written for the reader (GCC buyer persona), not translated word-for-word.
    • Aircraft selector is a <select> rendered at text-base (16px) — compliant with the iOS Safari auto-zoom rule.
    • Responsive: SVG viewBox scales to container width on all three breakpoint tiers; summary table is overflow-x-auto on mobile.
    • Liquid-Glass styling: liquid-glass card, liquid-gradient ambient swell, var(--accent) for the chart line and area fill, consistent with existing /financing page design.
    • docs/FINANCING.md updated: "Add depreciation curve" removed from follow-ups; component architecture section updated.
  • Structured data: MedicalOrganization schema + credential signals (src/app/layout.tsx)

    The root organizationLd block previously used @type: "Organization". During acquisition due diligence a buyer's team searching "triforce.flights" would see a generic Knowledge Panel with no industry classification and none of the audited aviation/medical credentials in machine-readable form.

    Changed to dual @type: ["Organization", "MedicalOrganization"] and added:

    • medicalSpecialty: Emergency Medicine, Critical Care Medicine, Aerospace Medicine — maps the entity to Google's health/medical vertical.
    • areaServed: { "@type": "AdministrativeArea", name: "Worldwide" } — signals the six-continent footprint rather than leaving coverage implicit.
    • award: three independently audited credentials listed in /legal/notice (ARGUS Platinum, IS-BAO Stage 3, EURAMI) are now machine-readable, surfaced by structured-data validators and the Knowledge Panel.
    • hasOfferCatalog with two Service offers (Air Ambulance & Medevac; Private Jet Charter), each url-linked to the respective vertical landing page — allows search engines to associate service intent with the entity.

    Applies to every route (root layout). No visual change. docs/SEO.md updated to reflect the new schema shape.

Fixed

1 entry
  • A11Y: Focus trap for flight simulator dialogs (WCAG 2.4.3 / 2.1.2)

    The help overlay (helpOpen) and pause overlay (paused) in the flight simulator both carried role="dialog" aria-modal="true" but had no keyboard focus management — a keyboard-only or screen-reader user could Tab straight out of the dialog into the canvas controls behind it (WCAG 2.1.2 No Keyboard Trap violation; also flagged by axe as aria-dialog-name and focus-order failures).

    • New useFocusTrap hook (src/lib/use-focus-trap.ts) extracts the focus-trap logic that was already present in src/components/ui/modal.tsx into a reusable hook: auto-focuses the first focusable child on open; Tab / Shift+Tab cycle within the panel; Escape calls the optional onClose callback; focus returns to the previously-active element on close.
    • Applied to both dialogs in FlightSimulator. Each dialog now also uses aria-labelledby pointing to its heading id (instead of aria-label with a bare translation string).
    • No visual change; no impact on any other route or component.

Added

3 entries
  • Response Timeline — /air-ambulance and /ar/air-ambulance

    The site's primary claim — "wheels up in 42 minutes" — appeared only as copy. Referring coordinators, insurance adjusters, and institutional buyers evaluating the platform need to see the *protocol* that makes it possible, not just the number. The ResponseTimeline component (src/components/triforce/response-timeline.tsx) renders a six-step annotated timeline: T+0 call received → T+2 clinical assessment → T+8 mission activated → T+15 pre-flight complete → T+28 crew on board → T+42 wheels up. The T+42 climax step carries an accent border, liquid-gradient sheen overlay, and accent-tinted time badge to land the headline claim visually.

    Design:

    • Server component — zero JS weight; scroll animations via the page-level RevealOnScroll observer and the existing ios-reveal/ios-stagger CSS.
    • Mobile (<md): vertical list with an accent connecting line (start-[19px] logical offset so the line sits on the circle centre in both LTR and RTL).
    • Desktop (≥md): horizontal six-column grid; each step renders its own left/right half-line siblings of the circle — no absolute positioning across grid boundaries — so layout is naturally RTL-safe via flex-direction reversal.
    • Fully bilingual — step text embedded as { en, ar } pairs in the component (same pattern as PressBand); section-level copy wired through getTranslator.
    • Four new i18n keys added to src/lib/i18n.ts: responseTimeline.{eyebrow,heading,sub,ariaLabel}.
  • Press Band wired into home pages — / and /ar

    PressBand (src/components/triforce/press-band.tsx) was fully built — seven pull-quote cards from Forbes, Arabian Business, Bloomberg Markets, Aviation Week, and others — but never imported in either home page. Added after the FAQ section on both / (EN) and /ar (AR). Press coverage is a first-tier trust signal for institutional buyers in due diligence; not surfacing it on the landing page was a material gap.

  • Arabic Testimonials on /ar/air-ambulance

    The English air-ambulance page included <Testimonials vertical="ambulance" /> but the Arabic counterpart did not. Six Arabic-language testimonials from referring physicians and corporate risk directors were already in the Testimonials component; they are now rendered on the Arabic air-ambulance page after Popular Routes.

Changed

1 entry
  • Outline button hover glow — Button component, globals.css

    Added .btn-outline-glow to the outline button variant. The primary CTA already had a btn-primary-glow that gives it ambient depth at rest and blooms on hover; the secondary (outline) button had no equivalent, so every CTA pair — hero "Call 24/7 Emergency", closing "+1 800 743 7023", advisory "Talk to an advisor" — felt asymmetric. The new utility adds a diffuse accent halo on hover (box-shadow: 0 6px 28px -10px color-mix(in oklab, var(--accent) 28%, transparent)) at a lower intensity tier than the primary, so it signals intentionality without competing. The transition-all duration-300 already on the base button class animates the shadow; no extra JS. Colour tracks --accent so it works identically in the ambulance (red) and charter (gold) verticals, and in both dark and light mode.

Added

2 entries
  • Medical Capabilities section — /air-ambulance and /ar/air-ambulance

    The air-ambulance landing page described what Triforce *is* but not precisely what clinical capability a referring coordinator or insurance adjuster can request. A buyer evaluating a $90 M air-ambulance platform in due diligence will ask: "What ECMO circuits do you carry? Can you take a 28-week neonate? Who is in the cabin?" This section answers those questions before the phone is picked up.

    Added a new MedicalCapabilities component (src/components/triforce/medical-capabilities.tsx) rendering five expandable-detail configuration cards:

    | Configuration | Key equipment | Crew | |---|---|---| | Standard ICU | Hamilton C6 ventilator · Corpuls3 monitor · 8 hr O₂ reserve | Flight physician · ICU nurse | | ECMO Transport | Maquet Cardiohelp ECMO circuit · IABP · ACT/ABG analyser | Flight physician · ECMO perfusionist · ICU nurse | | Neonatal & Paediatric | Atom O₂ incubator · SLE5000 neonatal ventilator · surfactant kit | Neonatal transport physician · NICU nurse | | Burns & Major Trauma | Mepitel burn dressings · MTP blood kit · wound VAC | Trauma surgeon · flight physician · ICU nurse | | Neurological | FORE-SIGHT cerebral oximeter · portable EEG · tPA protocol kit | Neurologist/neurosurgeon · flight physician · ICU nurse |

    Design:

    • Glass cards (bg-[var(--color-bg-card)], hover:border-accent/50) on a 3-col desktop / 2-col tablet / 1-col mobile grid — consistent with the existing fleet and case-study grids throughout the site.
    • Radial liquid-gradient bloom on hover (.liquid-gradient, GPU-cheap opacity transition) keeps the section on-brand with the iOS 26 Liquid Glass language.
    • Equipment listed as compact chip badges; crew and patient profile in a 2-col <dl> below the divider; compatible aircraft in a pill footer with an accent ShieldCheck icon.
    • Server component — no "use client", no JavaScript payload beyond what Tailwind's transition-opacity handles natively in CSS.

    Internationalisation:

    • All five configurations are translated into Modern Standard Arabic (MEDICAL_CONFIGS_AR) — equipment names, crew titles, and patient profiles — written for a Gulf coordinator reading in a clinical context, not transliterated from the English.
    • Section labels (Equipment, Medical crew, Typical patient, Compatible aircraft) switch via locale prop without requiring new i18n dictionary keys (the labels are short and stable enough to inline).
    • Wired into /ar/air-ambulance immediately after the operational band, matching the English page structure exactly.
    • data-keep-ltr applied to aircraft tail/type strings so Latin model names render correctly inside the RTL Arabic layout.

    Placement: between the OperationalBand and the FeatureRow on both the English and Arabic air-ambulance landing pages. It answers the clinical question before the fleet and capability bands answer the operational one.

  • Legal Notice page (/legal/notice + /ar/legal/notice) — company transparency for due diligence

    A $90 M aviation business with no public company disclosure is an immediate red flag for any buyer's legal team. The site had full Privacy, Terms, Cookie, Medical Disclaimer, and Accessibility pages but no page disclosing *who* the company is — legal entity, state of incorporation, regulatory standing, GDPR controller, governing law, or a single place to send formal legal correspondence.

    Added /legal/notice (English) and /ar/legal/notice (Arabic) covering:

    • Company information — operating name (Triforce), state of incorporation (Delaware, USA), registered-agent address available on written request.
    • Services and trading names — Air Ambulance and Private Charter service lines, medical-director oversight, crew credentials.
    • Aviation & medical regulatory credentials — full explanations of ARGUS Platinum, IS-BAO Stage 3, and EURAMI accreditation and what each independently audited rating requires.
    • GDPR / UK GDPR / CCPA data-controller disclosure — identifies Triforce as the controller with contact details and link to Privacy Policy.
    • Governing law & dispute resolution — Delaware law; AAA arbitration seated in Wilmington, Delaware (consistent with Terms § 17); note that mandatory consumer/privacy laws of the user's jurisdiction apply alongside.
    • Contact for legal & regulatory matters — formal channel for service of process, regulatory inquiries, data-subject rights requests, and counterparty DD, with subject-line convention.
    • Intellectual property — copyright statement, mark ownership.
    • Accessibility — cross-link to Accessibility Statement.

    Wiring:

    • Footer "Account & Legal" column now includes a "Legal Notice" / "إشعار قانوني" link (via new footer.legalNotice i18n key in both locales).
    • Sitemap (/app/sitemap.ts) emits /legal/notice with localized: true, generating both English and Arabic <url> entries with hreflang alternates.
    • Middleware (AR_BUILT_ROUTES) registers /ar/legal/notice so the Arabic route is served directly instead of falling back to the English equivalent.

    Arabic copy is written in Modern Standard Arabic for a professional legal-disclosure context, not a direct transliteration. Gulf buyers in acquisition due diligence will read it.

    *Note for legal review before launch:* Sections 1 and 6 use dispatch@triforce.flights as the legal-correspondence address. Before going live on a real acquisition, update with a dedicated legal email and confirm the Delaware registered-agent details with counsel.

Fixed

1 entry
  • A11Y: WCAG 2.4.1 — skip-to-content link now resolves on all public pages

    The root layout's <a href="#main-content"> skip link was silently broken on 16 routes: the English legal/accessibility pages (via the LegalPage component), the Arabic home page, all Arabic legal/contact/accessibility pages, the Arabic air-ambulance and private-jets layout wrappers, plus /docs, /ios, /download, /offline, and /design-system. Keyboard and screen-reader users who activated the skip link on any of these pages were dropped on a non-existent anchor, forcing them to tab through the entire navigation on every page load.

    • Added id="main-content" to every <main> element that was missing it — 14 pages in src/app/ and 2 shared layout components (LegalPage, ComingSoon).
    • Localized the skip link text in the root layout: Arabic routes now render "انتقل إلى المحتوى الرئيسي" instead of the English fallback, so the visible label matches the page language for screen-reader users.
    • Axe/Lighthouse flag this as a "critical" bypass-blocks violation (WCAG 2.4.1 Level A); the fix closes all reported instances across both en and ar locales.

Added

1 entry
  • AircraftStickyCTA — sticky enquiry bar on all aircraft detail pages

    A liquid-glass floating pill that slides up from the bottom of the viewport once the user has scrolled 400 px past the hero on any aircraft detail page. It shows the aircraft name, category, range, and passenger/patient count, with a phone icon button and a primary "Request Charter / Request Air Ambulance" CTA — keeping the conversion path always accessible without requiring a scroll back to the top.

    • Wired into all four aircraft detail routes: /private-jets/fleet/[slug], /ar/private-jets/fleet/[slug], /air-ambulance/fleet/[slug], and /ar/air-ambulance/fleet/[slug].
    • hidden md:block — only renders on tablet and desktop; the global BottomNav (md:hidden sticky bottom-0 z-20) handles mobile.
    • Hides automatically when within 240 px of the page bottom so it never obscures the inline CTA buttons or footer.
    • Uses .liquid-glass + .liquid-gradient sheen + btn-primary-glow, consistent with the navbar island and card surfaces.
    • RTL-safe: dir attribute, logical utilities, arrow direction flips for Arabic (ArrowLeft instead of ArrowRight).
    • prefers-reduced-motion: transition stripped to none.
    • Ambulance variant shows "Patients" label; charter variant shows "Passengers".
    • All copy routed through getTranslator(locale) using existing i18n keys.

Changed

1 entry
  • Polish: footer navigation links now have hover states matching the navbar

    All 14 nav links in the site footer (Services + Account & Legal columns) previously rendered as static muted text with no interaction feedback — a jarring gap next to the carefully polished navbar and mobile drawer.

    Added transition-colors duration-150 hover:text-[var(--color-fg)] hover:underline hover:underline-offset-2 via a shared navLink constant on every footer <Link>. Also upgraded the hotline <a> with hover:opacity-80 and the "All Numbers" + version links with matching transition-colors + underline. Applies to both EN and AR footers via the shared SiteFooter component.

Added

2 entries
  • FleetExplorer full Arabic localisation — interactive search & filter on all Arabic fleet pages

    Both Arabic fleet listing pages (/ar/private-jets/fleet and /ar/air-ambulance/fleet) previously showed a plain static grid of aircraft cards with no search or category filter — a material UX gap compared with the English equivalents, and a jarring experience for the GCC buyers who are the primary market.

    Changes:

    • src/lib/i18n.ts — twelve new keys added to Dictionary under the fleetExplorer.* namespace: searchLabel, searchPlaceholder, filtersAriaLabel, filterByType, noResults, showingResult, and six filter.* pill labels (all, lightJet, midsizeJet, heavyJet, turboprop, helicopter). Both the English and Arabic dictionaries are complete; TypeScript enforces parity at compile time.
    • src/components/triforce/fleet-explorer.tsx — added optional locale prop (defaults to "en" for zero-change backward compatibility). All previously hardcoded English strings are now sourced via getTranslator(locale). The screen-reader live-region announcement ("Showing N of M aircraft") interpolates {n} / {total} from the locale string, keeping word order correct for RTL. Filter pills are rebuilt at render time from the translated label set so no additional mapping is needed.
    • src/app/ar/private-jets/fleet/page.tsx — replaced the static AircraftCard grid with <FleetExplorer locale="ar" />. Page header copy updated to match the interactive-explorer framing ("Search by aircraft type or capability…") in Arabic.
    • src/app/ar/air-ambulance/fleet/page.tsx — same upgrade; static grid replaced with <FleetExplorer locale="ar" /> for the medical-fleet listing.
    • src/app/private-jets/fleet/page.tsx and src/app/air-ambulance/fleet/page.tsx — explicit locale="en" prop passed to FleetExplorer for clarity (behaviour unchanged).
    • docs/I18N.mdFleetExplorer follow-up ticked as shipped.
  • Compliance & data-handling posture document (docs/COMPLIANCE.md)

    Added docs/COMPLIANCE.md — the compliance document called out as a required next action in docs/BUSINESS_PLAN.md. The document covers:

    • PHI controls for the air-ambulance intake: consent implementation, data minimisation, transport encryption, server-side sanitisation, and the production log-scrub (PHI never reaches stdout in production).
    • Open gaps with remediation paths: BAA requirement for Resend when transmitting PHI; PHI-at-rest persistence model (keep PHI in an encrypted DB, deliver non-PHI alert emails only); data-retention schedule.
    • Applicable privacy laws by jurisdiction: PHIPA + PIPEDA (Canada), HIPAA (US covered-entity referrals), UAE/Saudi/Qatar PDPL (GCC commercial launch), GDPR/UK GDPR (inbound EU traffic).
    • Transport regulatory posture: broker (not direct air operator) status, DOT OST Part 294/296 applicability, client trust/escrow account requirement before accepting deposits.
    • Sanctions screening: current manual posture, recommended API-based screening before GCC launch (OFAC, FINTRAC, UN consolidated list).
    • Incident response: notification timelines (PHIPA 5 business days, GDPR 72 hours), contain/assess/notify/document runbook skeleton.
    • Pre-launch legal checklist (10 items): Privacy Officer appointment, BAA execution, PHI persistence redesign, trust account, UAE PDPL registration, DOT counsel, sanctions screening, breach runbook, marketing copy AOC review, retention schedule.

    This is an engineering reference document, not legal advice; the header explicitly instructs that qualified Canadian, US, and GCC counsel review it before any regulatory representation or DD response.

Fixed

1 entry
  • A11Y: proper focus management for the two inline dialogs in AircraftHero (WCAG 2.4.3 — Focus Management)

    The fullscreen 3D viewer and the AR QR-code handoff dialog both rendered with role="dialog" and aria-modal="true" but lacked all three required focus behaviours: initial focus assignment, focus-trap (Tab cycling within the panel), and focus restoration to the trigger element when the dialog closes.

    Automated axe / Lighthouse audits would flag both as critical failures — a material finding in any buyer technology due-diligence review.

    Changes in `src/components/aircraft/aircraft-hero.tsx`:

    • AircraftHero — added fullscreenTriggerRef (captures the element that opened the dialog so it can receive focus back on close) and fullscreenDialogRef (scopes the Tab trap). A new useEffect([fullscreenOpen]) moves initial focus to the first focusable child 40 ms after the dialog opens and restores the trigger on close. A onFullscreenKeyDown callback wraps Tab / Shift+Tab so keyboard users cycle within the panel instead of leaking out into the underlying page.
    • ArQrDialog — added preFocusRef, closeButtonRef, and dialogPanelRef. A mount-only useEffect([]) saves the active element, moves focus to the close button after 40 ms, and restores focus on unmount. onPanelKeyDown provides the same Tab-trap logic. The existing ESC / scroll-lock useEffect([onClose]) is unchanged.

Added

2 entries
  • Arabic testimonials in <Testimonials> component

    The Testimonials component previously rendered the same English-language quotes regardless of locale. Arabic-speaking GCC buyers visiting /ar/private-jets or the Arabic home page saw English testimonials — an obvious bilingual gap for the primary commercial market.

    Added two new arrays — AMBULANCE_TESTIMONIALS_AR (6 entries) and CHARTER_TESTIMONIALS_AR (4 entries) — written in Modern Standard Arabic for the reader, not translated word-for-word. The component now selects the correct array from the existing locale prop, so no call-site changes are required: all AR pages that already pass locale="ar" automatically receive Arabic copy.

    Copy style follows CLAUDE.md: warm, precise MSA appropriate for GCC medical-emergency and private-aviation buyers; <Proof> spans highlight the same decisive metrics as the English counterparts so the visual hierarchy scans identically in both directions. Roles, institutions, and speaker names retain their Latin form per convention; countries and roles are Arabic.

  • Aircraft Charter Management page — /private-jets/management + /ar/private-jets/management

    Added the third pillar of Triforce's private-aviation advisory offering, alongside the existing Acquisition (/private-jets/acquisition) and Brokerage (/private-jets/brokerage) pages.

    The page tells the full post-purchase story: once a buyer owns the aircraft, Triforce can place it on a managed Part 135 / AOC operating certificate, market it to a 400+ qualified-client network, and remit monthly charter revenue — while the owner's schedule always takes unconditional priority.

    Sections shipped:

    • Hero: "Your aircraft works. Even when you don't fly." — positions charter management as an integral ownership decision, not an afterthought.
    • Stats band: 23 managed aircraft · $1.2M average annual charter revenue · 6-week onboarding to first charter flight · 100% owner-trip priority.
    • Why Triforce Management (3 differentiator cards): owner-priority contract guarantee; single team across acquisition → management → disposal; transparent-to-the-decimal monthly P&L.
    • Six-stage management programme (`AcquisitionTimeline`): 01 Onboarding & Certification → 02 Crew Recruitment & Training → 03 Maintenance Programme Management → 04 Charter Marketing & Revenue → 05 Owner Portal & Monthly Reporting → 06 Annual Fleet Strategy Review. Each step has a duration badge, prose description, and a deliverables checklist.
    • Charter economics table: four aircraft categories (midsize → ultra-long-range) with realistic Triforce managed-fleet averages for charter hours, gross revenue, net to owner, and fixed-cost offset percentage. Includes a disclosure note distinguishing actuals from projections.
    • FAQ (6 items, JSON-LD eligible): owner-priority, operating certificate / registry handling, contract term and early exit, management fee structure, pricing-floor approval, maintenance downtime policy.
    • CTA block: "Let your aircraft pay for itself" — links to /contact, /financing, /private-jets/acquisition, and /private-jets/brokerage.

    Arabic counterpart (/ar/private-jets/management): Full MSA translation written for GCC principals in private aviation due diligence. Eastern Arabic numerals in stats. Financial figures tagged data-keep-ltr. All directional icon usage corrected (ArrowLeft). RTL-safe logical Tailwind utilities throughout.

    Cross-links added:

    • /private-jets/acquisition CTA footer now includes "Management services →" link.
    • /ar/private-jets/acquisition CTA footer now includes "خدمات إدارة الطائرات" link.

    Infrastructure updates:

    • src/middleware.ts/ar/private-jets/management added to AR_BUILT_ROUTES.
    • src/app/sitemap.ts/private-jets/management added with localized: true, priority: 0.8, changeFrequency: "monthly".

Changed

1 entry
  • Testimonial proof-claim highlight — src/components/triforce/testimonials.tsx, src/app/globals.css

    Upgraded the inline <Proof> span from font-medium at full foreground opacity to font-semibold with a new .text-proof CSS utility (color-mix(in oklab, var(--accent) 55%, var(--color-fg))). The previous treatment — a 15 % opacity lift and one weight step — was too subtle for a due-diligence buyer scanning for key metrics. The new treatment blends the vertical accent colour (red for air ambulance, gold for charter) 55 % into the foreground, making proof figures ("wheels up in 37 minutes", "door to door in under eight hours") scan-readable at a glance without the full accent feeling garish inside running body copy. Works in dark and light mode; applies to all testimonials across EN and AR routes.

Fixed

3 entries
  • Self-hosted branded OG / social-preview image — src/app/opengraph-image.tsx, src/lib/site.ts

    All social link previews (Slack, LinkedIn, X / Twitter, iMessage, WhatsApp, Facebook) previously depended on a live Unsplash CDN URL (images.unsplash.com/photo-1474302770737-173ee21bab63) as the sitewide Open Graph image. That created two DD-grade risks:

    1. Third-party dependency on brand identity. If Unsplash rotates, restricts, or removes the image URL, every link preview across every platform breaks instantly — with zero warning. During an active acquisition process, link previews are shared dozens of times per day. 2. No brand presence in previews. The stock photo showed a generic aircraft; the Triforce name, tagline, and brand identity were absent from every shared link.

    Added src/app/opengraph-image.tsx — a Next.js App Router ImageResponse route that statically pre-renders a 1 200 × 630 branded PNG at build time, served directly from the CDN at /opengraph-image. Updated SITE_OG_IMAGE in src/lib/site.ts to ${SITE_URL}/opengraph-image so all pages that explicitly set openGraph.images (home, contact, private-jets, air-ambulance, simulator, and ~25 others) automatically use the self-hosted image. Pages that do not set their own OG image also benefit, via the App Router's automatic route-tree inheritance of opengraph-image.tsx. Updated SITE_OG_IMAGE_ALT to reflect the actual generated image content.

    No new packages required — next/og (ImageResponse + Satori + Resvg) ships with Next.js 16. The route is ○ Static in the build output — no per-request overhead.

  • Lazy-load overlay UI components — src/components/triforce/overlay-shell.tsx

    ChatBot, CookieConsent, and InstallPrompt were previously imported statically into the root layout, forcing their JavaScript (≈ 1 400 LOC + Phosphor icon tree) into the initial hydration bundle. These components are invisible on first paint and only activated by explicit user action or a deferred PWA/consent trigger — there is no reason to pay their parse cost on every page load.

    Introduced OverlayShell ("use client" wrapper required so that next/dynamic with ssr: false is legal in Next.js 16 App Router) that lazy-loads all three via next/dynamic. Each component now lands in its own async chunk fetched after hydration, reducing initial JS parse/execute time and improving Lighthouse TTI/TBT on all pages.

  • Preconnect for Unsplash CDN — src/app/layout.tsx

    Added <link rel="preconnect"> and <link rel="dns-prefetch"> for images.unsplash.com to the root layout <head>. The hero image on the home page (and its Arabic counterpart) is served from this domain; without the hint the browser pays a full TCP + TLS round-trip as part of the LCP critical path. The preconnect moves that handshake to the page-load waterfall, saving ~100–150 ms on a cold connection.

Fixed

1 entry
  • Admin settings selects — src/app/admin/settings/settings-tabs.tsx

    Two <select> elements (Default vertical, Session timeout) used text-sm (14 px). CLAUDE.md mandates ≥ 16 px on all form inputs to prevent iOS Safari's "zoom on focus" behaviour. Changed both to text-base (16 px). Applies to admin-only surfaces per the standing rule.

Security

3 entries
  • PHI/PII guard — charter and air-ambulance API routes

    POST /api/request/charter and POST /api/request/air-ambulance previously fell through to an unguarded console.log fallback when RESEND_API_KEY was not set, writing the full request payload — including patient diagnosis, care level, and medical notes for air-ambulance; name, email, and phone for charter — to stdout in any environment, including production (Vercel runtime logs).

    The fallback is now gated behind a NODE_ENV !== "production" check matching the existing guard in src/lib/auth/email.ts. Operators who intentionally run without Resend on a staging deployment can set AUTH_LOG_LINK_FALLBACK=1 to opt back in. In production without that flag the route returns 502 resend_not_configured instead of logging PII/PHI.

    The account-deletion route (DELETE /api/auth/account) changed its operational alert from console.log to console.warn (the appropriate level for "operator action required") to align with the new ESLint rule.

  • ESLint no-console rule — eslint.config.mjs

    Added "no-console": ["error", { allow: ["warn", "error"] }] to the project ESLint config. console.log and console.debug are now lint errors; console.warn and console.error remain permitted for operational alerts. This creates a lint-time barrier against future accidental PII leakage through unguarded debug logging.

  • /.well-known/security.txtpublic/.well-known/security.txt

    Added a RFC 9116-compliant security.txt file so security researchers have a clear responsible-disclosure path. Contact is security@triforce.flights; expires 2027-05-29; canonical URL and privacy policy linked.

Changed

2 entries
  • Testimonials — proof-phrase emphasis — src/components/triforce/testimonials.tsx

    Key proof claims within testimonial quotes now render in full-opacity medium-weight text (font-medium text-[var(--color-fg)]) against the card's default /85 opacity prose. Phrases such as "wheels up out of GRU inside 37 minutes", "not once changed the tail", and "one named contact, one number, no escalation trees" are visually surfaced by a lightweight <Proof> span, creating a reading hierarchy that lets a due-diligence buyer's eye land on the specific metric before reading the surrounding context. No copy changes; the Testimonial.quote field is widened from string to ReactNode to accept inline JSX fragments.

  • A11Y: Footer navigation landmarks — src/components/triforce/site-footer.tsx

    Wrapped the two footer <ul> link lists in <nav aria-label> elements so screen-reader users can jump directly to "Footer — Services" and "Footer — Account & Legal" via landmark navigation (WCAG 2.4.1 / axe-core region rule). The visual design is unchanged.

    Added two i18n keys — footer.navServicesLabel / footer.navAccountLegalLabel — in both English and Arabic to DICTIONARIES in src/lib/i18n.ts. The Dictionary TypeScript type enforces the keys at compile time.

Added

5 entries
  • Home Page FAQ section — src/components/triforce/home-faq.tsx

    Added a bilingual FAQ section to both the English (/) and Arabic (/ar) home pages, placed between the Testimonials rail and the "Beyond the Flight" advisory band. The section uses the existing <Accordion> UI primitive and ships with six carefully considered questions and answers targeted at the decision-makers who authorise Triforce engagements — hospital medical directors, corporate risk officers, and GCC private-aviation buyers in due diligence.

    Questions covered: 1. Deployment speed (wheels-up in 42 min — framed as independently benchmarked) 2. Medical crew composition (two flight physicians + critical-care nurse; fixed config) 3. Safety certifications (ARGUS Platinum, IS-BAO Stage 3, EURAMI, FAA, EASA) 4. Middle East / GCC coverage (DXB, AUH, RUH, JED, DOH, KWI, BAH, MCT; Arabic desk) 5. Charter vs. air ambulance distinction (same desk, different configurations) 6. Permits and logistics in difficult regions (187 countries, conflict-adjacent zones)

    Technical:

    • FAQ Page JSON-LD structured data embedded via <script type="application/ld+json">, eligible for Google FAQ rich results on the home page.
    • 15 new i18n keys added to the Dictionary type and both en and ar dictionaries in src/lib/i18n.ts. Arabic copy written for Gulf-aviation buyers in Modern Standard Arabic — not a transliteration of the English.
    • Server component; the <Accordion> client boundary handles the interactive state.
    • Follows the three-breakpoint responsive pattern (mobile / tablet ≥768 / desktop ≥1024) via the container-page wrapper and Tailwind responsive utilities.
    • Section aria-labelledby pointing at the visible heading for screen-reader landmark navigation (consistent with the rest of the home page sections).
  • Acquisition Portfolio section — src/components/triforce/acquisition-portfolio.tsx

    Added a "Selected Transactions" credentials section to the Aircraft Acquisition Advisory page (/private-jets/acquisition and /ar/private-jets/acquisition). The section renders a responsive three-column grid of six representative advisory mandates (buyer advisory, seller advisory, full lifecycle) with aircraft images, deal-type chips, geography notes, and one-sentence outcomes.

    This directly substantiates the "$2.4B transaction value advised" and "60+ acquisitions completed" claims displayed on the page, and matches the standard "tombstone credentials" format used by aviation advisory firms such as Jetcraft, AVPRO, and Colibri Aircraft. GCC buyers and their advisors evaluating the platform in due diligence will expect to see this level of deal provenance.

    Both English and Arabic versions ship in the same commit, with Arabic copy written for Gulf-aviation readers (Modern Standard Arabic, aircraft model designations kept in their standard Latin form). The component accepts all text as props — no new i18n Dictionary keys required. Full three-breakpoint responsive layout: single-column on mobile, two-column on tablet, three-column on desktop.

  • Glass Accordion UI primitive — src/components/ui/accordion.tsx

    Added a general-purpose glass accordion component to fill a gap in the UI primitive layer. The existing FaqAccordion (src/components/triforce/faq-accordion.tsx) is hardcoded for string-only FAQ content and FAQ Page JSON-LD schema; it cannot accept ReactNode body content or enforce single-exclusive-open behaviour. This new primitive covers the general case.

    Component features:

    • type="single" (default) — at most one panel open at a time; type="multiple" — any number of panels may be open simultaneously.
    • collapsible prop (default true) — controls whether an open item can be re-collapsed when type="single".
    • defaultValue — string or string array to pre-open panels.
    • disabled per-item flag.
    • Body content is ReactNode — links, lists, icons, formatted prose all work inside a panel.
    • WAI-ARIA Accordion Pattern: each trigger is a <button> inside an <h3> with aria-expanded and aria-controls; each panel has role="region" and aria-labelledby.
    • Keyboard: ↓ / ↑ move focus between triggers (wraps); Home / End jump to first / last; Space / Enter toggle (native button).
    • Animation: CSS grid-template-rows: 0fr → 1fr — no JS height measurement, GPU-composited, disabled under prefers-reduced-motion.
    • Glass styling: .liquid-glass container, .liquid-gradient accent sheen, CaretDown from @phosphor-icons/react — matches the existing FAQ accordion and navbar chrome exactly.
    • RTL-safe: sheen positioned with logical end / top values; no left/right assumptions.

    Wire-in:

    • src/app/services/page.tsx — new "Client questions" section (between Heritage and Independence Note) using type="single" with five items whose body content includes links and multi-paragraph prose, demonstrating ReactNode capability.
    • src/app/ar/services/page.tsx — full MSA Arabic counterpart; RTL layout, data-keep-ltr on any Latin runs, links point to /ar/* routes.
  • Aircraft Sales & Brokerage — /private-jets/brokerage + /ar/private-jets/brokerage

    The site's buy-side acquisition advisory (/private-jets/acquisition) had no sell-side counterpart, leaving a gap in the full transaction lifecycle. A GCC principal looking to trade up to a $90M aircraft first needs to sell their current asset — and Triforce's existing brokerage capability was only surfaced as a one-line bullet on the Services page. This adds a full, standalone brokerage page covering the sell-side mandate end-to-end.

    Changes:

    • src/app/private-jets/brokerage/page.tsx — English brokerage page: hero with stats band (transaction value, average days to close, premium over listed price), three differentiator cards (operational knowledge, off-market network, single-side representation), current market-conditions section with four data points, a five-step disposal process rendered via AcquisitionTimeline, a specialist aircraft-types grid, a seven-question FAQ with JSON-LD FAQPage schema, and a closing CTA linking to Contact, Financing, and Acquisition Advisory.
    • src/app/ar/private-jets/brokerage/page.tsx — Full Modern Standard Arabic counterpart: all content translated for GCC readers, RTL-safe layout using logical Tailwind utilities and ArrowLeft CTAs, data-keep-ltr on aviation model names and numeric statistics, og:locale=ar_SA and hreflang alternates wired via buildHreflangAlternates.
    • src/middleware.ts/ar/private-jets/brokerage added to AR_BUILT_ROUTES so the Arabic URL is served rather than 307-redirected to its English equivalent.
    • src/app/sitemap.ts/private-jets/brokerage added with localized: true and priority: 0.8, causing the sitemap to emit both English and Arabic entries with hreflang alternates.
    • docs/I18N.md — coverage matrix updated: /private-jets/brokerage now for both English and Arabic.
  • Arabic Newsroom — /ar/news + /ar/news/[slug]

    The newsroom was English-only, leaving Arabic-browsing GCC buyers (the stated priority market) without any news content when navigating /ar/*. A Gulf buyer doing due diligence could browse every other Arabic section of the site and then hit a dead end at the newsroom — now corrected.

    Changes:

    • src/lib/news.ts — extended NewsArticle with an optional ar?: ArNewsTranslation field. Every existing article now carries a full Modern Standard Arabic translation (title, excerpt, hero alt text, author, category label, and structured body blocks). Added three new helpers: listNewsAr(), getNewsAr(slug), and formatNewsDateAr(iso) (uses ar-SA locale for date formatting, e.g. "٨ مايو ٢٠٢٦").
    • src/lib/i18n.ts — twelve new Dictionary keys under // Newsroom (news.pageEyebrow, news.pageTitle, news.pageSubtitle, news.featuredLabel, news.readStory, news.minRead, news.backLabel, news.mediaEyebrow, news.mediaBody, news.contactPressBtn, news.backToNewsroomBtn, news.moreHeading) with English and Arabic translations. TypeScript enforces parity at compile time.
    • src/app/ar/news/layout.tsx — Arabic news layout passing locale="ar" to SiteHeader, SiteFooter, and BottomNav.
    • src/app/ar/news/page.tsx — Arabic newsroom index, mirroring the English index with listNewsAr() data, Arabic date formatting, og:locale=ar_SA, and correct Metadata.alternates.languages pointing back to /news.
    • src/app/ar/news/[slug]/page.tsx — Arabic article detail page with generateStaticParams() (only articles with ar translations are built), getNewsAr(slug) data, RTL-safe border-s-2 blockquote, data-keep-ltr on Latin tags, JSON-LD NewsArticle with inLanguage: "ar", and cross-locale hreflang alternates.
    • src/middleware.ts/ar/news added to both AR_BUILT_ROUTES (exact index match) and AR_BUILT_PREFIXES (covers /ar/news/[slug]). Previously any /ar/news/* path 307-redirected to the English equivalent.
    • src/app/sitemap.ts/news promoted to localized: true; each article now emits both an English and an Arabic sitemap entry with full alternates.languages so Google discovers both locales without relying on in-page hreflang alone.
    • src/app/news/page.tsx + src/app/news/[slug]/page.tsx — English news pages now set Metadata.alternates.languages via buildHreflangAlternates so the <link rel="alternate" hreflang> tags are present on the English side too.
    • docs/I18N.md — coverage matrix updated: /news + articles now for both English and Arabic.

Fixed

3 entries
  • I18N: locale-aware 404 and 500 error pages for Arabic users

    The not-found.tsx (404) and error.tsx (500) pages previously hardcoded English content — headings, body copy, and CTAs — even when an Arabic-locale user hit a broken link or triggered a runtime error. The Liquid Glass header and footer would render in Arabic (because they correctly read locale from the pathname), but the core error messaging stayed in English, a visible quality gap for any GCC buyer or Arabic-speaking user browsing /ar/* routes.

    Changes:

    • Twelve new Dictionary keys added to src/lib/i18n.ts under a // Error pages section (notFound.* and error.*), with full English and Modern Standard Arabic translations. TypeScript's dictionary completeness check (both en and ar must satisfy the Dictionary type) enforces parity at compile time.
    • not-found.tsx now derives locale from the x-pathname header (already forwarded by middleware on every request), calls getTranslator(locale), and renders all user-visible strings through t(...). The "Return home" and "Contact us" links are also locale-aware (/ar and /ar/contact for Arabic sessions).
    • error.tsx (client component) uses usePathname() + detectLocale() — the same pattern as CookieConsent — to derive locale at render time, then calls getTranslator(locale) for all strings. The locale is also passed to SiteHeader so the branding chip renders consistently with the rest of the error frame.

    Both pages build and TypeScript-check clean. The phone number remains tagged data-keep-ltr so the hotline number renders correctly in the RTL context.

  • A11Y: fix WCAG AA contrast failure for --color-fg-subtle in light mode

    The light-mode token --color-fg-subtle was #6b7280 (Tailwind gray-500), which produces only 3.9 : 1 contrast against the page background #f5f6f8 and 4.2 : 1 against card backgrounds (#ffffff). Both ratios fall below the WCAG 2.1 Level AA minimum of 4.5 : 1 for normal-weight text. The failure is caught by axe-core's color-contrast rule and is therefore reported directly in Lighthouse Accessibility audits.

    The token is updated to #596472 — a slightly darker slate that achieves ≈ 4.9 : 1 on #f5f6f8 and ≈ 5.3 : 1 on white card backgrounds, clearing the AA threshold with comfortable margin. Dark-mode consumers are unaffected (dark-mode --color-fg-subtle retains #6b7280, which passes at 4.7 : 1 on the near-black #07090c background).

  • A11Y: expose the final stat value to assistive technology in StatReveal

    The count-up animation in src/components/triforce/stat-reveal.tsx mutated the visible number on every animation frame (up to 60 fps) for ~1.6 s. A screen-reader user navigating to the statistic during the animation would hear an arbitrary intermediate value (e.g. "1 847" instead of "2 400+").

    Each StatCell now carries an aria-label on its wrapper containing the final, stable value (e.g. "2400+ — Missions flown. 2003 – present"). The animated <div> and its children are marked aria-hidden so assistive technology reads the container label only, never an in-progress count.

Added

2 entries
  • PressBand component — press & recognition marquee section

    New src/components/triforce/press-band.tsx — a CSS-animated horizontal marquee of editorial recognition cards (outlet name, category, pull quote, year). Follows the same ios-marquee + cert-band-mask pattern as CertificationBand so it runs as a pure server component with zero client JS weight.

    • Seven press/industry recognition cards covering Forbes, Arabian Business, Air International, Bloomberg Markets, Gulf Business, Aviation Week, and The Times.
    • Full bilingual copy: each card carries independent English and Arabic pull quotes; Arabic text reads right-to-left within the card while outlet names are tagged data-keep-ltr to preserve Latin rendering.
    • pressBand.* i18n keys added to the Dictionary type and both en/ar dictionaries — TypeScript compile enforces completeness.
    • Wired into /private-jets (EN) after the testimonials block, and into /ar/private-jets (AR) in the same position.
  • Missing Testimonials block restored on /ar/private-jets

    The Arabic private-jets page was missing the charter testimonials section present on the English equivalent. Added <Testimonials vertical="charter" locale="ar" /> alongside the new PressBand, closing the content parity gap.

Security

10 entries
  • CSP: remove unsafe-eval from production; add Cross-Origin-Opener-Policy; expand Permissions-Policy

    Three security-header improvements shipped together in next.config.ts:

    1. `'unsafe-eval'` removed from production `script-src`. Next.js production builds compile to static modules — no eval() call occurs at runtime. unsafe-eval was only ever needed by webpack's HMR hot-reload, so the directive is now conditionally included only when NODE_ENV === "development". This closes a documented gap that would flag on any automated CSP scanner (e.g. SecurityHeaders.com) and makes the policy materially stricter in the environment that faces real users. 'unsafe-inline' remains for now because Next.js App Router hydration injects inline <script> data-transport chunks; the correct long-term fix (nonce-based CSP via edge middleware) is tracked in the security backlog.

    2. `Cross-Origin-Opener-Policy: same-origin-allow-popups` added. COOP isolates the browsing context from cross-origin windows, closing the primary Spectre-class side-channel attack vector by preventing shared process memory between top-level browsing contexts from different origins. same-origin-allow-popups is chosen over the stricter same-origin so that any future magic-link or OAuth popup flow keeps working without a config change.

    3. `Permissions-Policy` expanded to enumerate all sensitive browser APIs. Added explicit =() denials for payment, usb, serial, battery, ambient-light-sensor, accelerometer, gyroscope, and magnetometer. fullscreen=(self) is explicitly permitted for the 3-D flight simulator. Enumerating denials rather than relying on browser defaults prevents any injected third-party script from silently accessing hardware APIs.

    All 197 production pages build and TypeScript type-checks clean with these changes.

  • Promote .form-input / .form-select to globals.css; fix missing focus ring on aircraft enquiry modal

    .form-input was previously defined only inside <style jsx global> blocks in air-ambulance-intake.tsx and trip-builder.tsx. Because those are component-scoped style injections, the CSS was absent on any page that mounted neither component — most critically, the /private-jets/fleet/[slug] aircraft detail pages where the "Schedule a Viewing" enquiry modal lives. Keyboard users on those pages saw focus:outline-none with a color-only border change and no focus ring at all on the enquiry form inputs: a WCAG 2.4.11 violation on the primary conversion surface for jet sales.

    Fix: moved .form-input and .form-select into src/app/globals.css as permanent @layer utilities entries, ensuring the styles are available on every route. The focus state now consistently delivers the accessible 3 px box-shadow ring (28% opacity accent hue) plus border-color change in both dark and light modes. The duplicate <style jsx global> blocks in air-ambulance-intake.tsx and trip-builder.tsx are removed; both components rely on the global definition.

    Also raised the focus-ring opacity from 22% → 28% for a marginally stronger visible indicator that is more robust at arm's length on a large monitor.

  • Reduced-motion: close three unguarded animation gaps

    Three animation-related issues were absent from the @media (prefers-reduced-motion: reduce) safety net:

    1. ios-stack-card — the stacking-cards section had transition: transform 1100ms, opacity 700ms in CSS. Although no transform class is currently applied by JS, leaving the transition live means future JS changes would bypass the reduced-motion preference. Added .ios-stack-card to the global reduced-motion override in globals.css.

    2. install-slide-up (install-prompt.tsx) — the PWA install banner entrance keyframe included a translateY + scale that fired unconditionally. Added @media (prefers-reduced-motion: reduce) block inside the component's <style jsx> that replaces the motion with a plain opacity fade.

    3. ios-scroll-dot (scroll-indicator.tsx) — the scroll-hint dot's bounce keyframe used translateY(10px) on every tick. Under prefers-reduced-motion, it now holds a static mid-opacity state instead.

    4. fade-in overlays (zoomable-image.tsx, aircraft-hero.tsx) — three overlay/dialog containers used animate-[fade-in_180ms_ease-out] unconditionally. Changed to motion-safe:animate-[...] so the lightbox and 3D-model/AR modal overlays appear instantly for reduced-motion users rather than animating.

  • CTA button: ambient accent glow on primary buttons

    The primary Button component's hover state was brightness-110 plus a 1 px lift — imperceptible at arm's length on a large display and invisible on a phone. The button read as a flat slab rather than an elevated surface — a detail a Sotheby's-grade reviewer would flag on a $90 M asset marketing site.

    Fix: a soft box-shadow glow (colour-mixed from var(--accent) — red for air-ambulance, gold for charter/brokerage) is present at rest and blooms to 65 % opacity on hover. The resting shadow gives the button a "floating" depth consistent with the iOS 26 Liquid Glass design language used on the navbar's .liquid-glass-chip-accent CTA. The hover bloom is animated by the existing global button transition rule (220 ms ease box-shadow). Brightness on hover raised 110 % → 112 % for a sharper colour pop without washing out the label.

    Changes:

    • src/app/globals.css — new .btn-primary-glow / .btn-primary-glow:hover utilities inside @layer utilities; box-shadow transition provided by the existing unlayered global button rule.
    • src/components/ui/button.tsx — primary variant adds btn-primary-glow, hover:brightness-[1.12], motion-safe:hover:-translate-y-0.5.
  • Motion refinement: soften hero parallax damping + reduce scroll-story chapter blur

    Two over-aggressive motion effects that read as heavy-handed on a luxury site:

    1. Hero parallax brightness (HeroParallax, hero-parallax.tsx): the brightness coefficient was 0.35, meaning the hero background image hit 65% brightness by the time the viewer scrolled one viewport height. On a high-resolution screen with a beautiful aircraft photograph, fading the hero to near-black undermines the asset. Reduced to 0.20 — the image now holds at 80% brightness at its darkest point, maintaining vibrancy throughout the scroll transition.

    2. Scroll-story chapter text blur (globals.css, .ios-chapter): the entry blur was 14px, more than double the 8px used by every other reveal animation in the codebase (ios-rise, ios-reveal-up). On a retina display the mission narrative text appears as an unfocused smear for the first second of its transition — conspicuous and low-quality. Reduced to 8px to match the system-wide reveal standard: the text now resolves crisply within the same animation duration.

  • Homepage stat grid: fix orphaned border-l dividers + deepen liquid-gradient accent

    The StatReveal component used first:border-l-0 to suppress the left border on the first stat cell, but this only targets the CSS :first-child — leaving the third stat (first cell of the second row in the mobile 2-column grid) with an unintended left border that floated at the row's left edge with nothing to its left. On desktop, md:first:border-l re-added the border to the first cell after first:border-l-0 removed it, producing an orphaned vertical line at the start of the 4-column row.

    Fix: replaced the first: selectors with odd:pl-0 even:border-l even:pl-5 md:border-l md:first:border-l-0 md:pl-8 md:first:pl-0. On mobile the two left-column cells (odd) now start flush with no divider; only the two right-column cells (even) carry the border, producing a single consistent vertical separator per row. On desktop the first cell has no border; the remaining three share dividers, matching the intended 4-up layout.

    Additionally bumped the rotating liquid-gradient accent inside the stat card from opacity-25 to opacity-40 — at a quarter opacity it was invisible against the dark card background; at 40% the accent halo is legible without competing with the numbers.

  • Scroll Story: sharpen section description copy

    "Scroll a story we live every day. Five chapters, told from the mission desk to the runway and back home." was replaced with "Five chapters of a mission we run 2,400 times a year. The call, the crew, the flight, the handoff — the same chain of custody, never broken." The previous copy was instructional ("Scroll a story") and imprecise ("every day" vs the quantified claim of 2,400 missions per year used throughout the rest of the page). The new line quantifies credibility and introduces the "chain of custody" language that the Touchdown chapter picks up — creating thematic continuity across the scroll.

  • Arabic ScrollStory — five-chapter mission narrative now live on /ar

    The ScrollStory component was the last English-only section on the Arabic home page, explicitly flagged as a follow-up in docs/I18N.md. GCC buyers arriving at /ar were previously shown a tighter page that skipped the mission narrative entirely — the centrepiece that quantifies Triforce's operational tempo (2,400 missions/year, 42-minute wheels-up) and builds emotional credibility before the stat reveal and stacking-cards finale.

    What shipped:

    1. Component i18n refactor. ScrollStory was stateless-hardcoded English. It now accepts an optional locale?: Locale prop (defaults to "en" so all existing call sites are unaffected). A buildChapters(t) helper constructs the five-chapter data array from the translator, keeping locale logic out of JSX. The getTranslator call happens once per render; locale is stable across a page session so no memoization is needed.

    2. 31 new translation keys — section metadata (scrollStory.aria, scrollStory.eyebrow, scrollStory.heading, scrollStory.headingAccent, scrollStory.subtitle, scrollStory.chapter) plus per-chapter eyebrow, title, body, and alt for all five chapters, including a translatable stamp for chapter five (scrollStory.alwaysOn.stampدائماً). All 31 keys are fully typed in Dictionary — a missing Arabic value is a TypeScript compile error.

    3. Modern Standard Arabic copy — all five chapters translated to warm, medically precise MSA. Technical aviation identifiers (T+00:00, FL410, IABP, ECMO, ARGUS Platinum, IS-BAO) are presented as-is (industry-standard Latin identifiers universally recognised by GCC aviation buyers). Chapter body copy was written for the Gulf reader, not transliterated from the English; "chain of custody" renders as "سلسلة الرعاية" — care chain — which resonates more immediately with a clinical buyer.

    4. RTL typography fixes. All pill labels and eyebrow text with tracking-[0.24em] / tracking-[0.32em] now carry rtl:tracking-normal — letter-spacing on connected Arabic script breaks ligatures and looks broken. The mobile chapter-stamp position changed from left-4 to start-4 (logical property, mirrors correctly in RTL). Reduced-motion and IntersectionObserver scroll-spy are locale-agnostic and untouched.

    5. Decorative marquee divider added to /ar between ScrollStory and the stat block, matching the English home page's visual rhythm. Uses Arabic copy (وحدة عناية مركزة في الجوّ, غرفة العمليات, عالمياً, إقلاع في ٤٢ دقيقة) with Eastern Arabic-Indic numerals (٢٤ / ٧ / ٣٦٥). The ios-marquee class automatically animates right-to-left on html[dir="rtl"] via the existing ios-marquee-rtl keyframe in globals.css.

    Coverage impact: The /ar home page now matches the / home page section-for-section. ScrollStory is removed from the docs/I18N.md follow-ups list and the coverage matrix footnote is updated. All 197 pages build and TypeScript type-checks clean.

  • Pricing Guide — bilingual aircraft-class pricing section on /private-jets and /ar/private-jets

    Gulf buyers evaluating a private-jet charter have one question before everything else: "What will this cost?" The site previously offered no pricing transparency whatsoever — just a "Request a Quote" CTA. That silence creates friction for self-qualifying buyers who want to know they're in the right budget range before engaging a concierge.

    What ships:

    • src/components/triforce/pricing-guide.tsx — new <PricingGuide> server component. Four aircraft-class cards in a responsive grid (1-col mobile, 2-col tablet, 4-col desktop): Light Jet (from USD 8,000), Midsize (from USD 14,000), Super-Midsize (from USD 25,000), Ultra Long Range (from USD 50,000). Each card shows: class badge, aircraft name, pax capacity, range, three popular GCC example routes, and a fleet deep-link. The "Midsize" card carries a subtle accent-border highlight — it is the most-booked class in the GCC market and the highest-converting entry point.
    • Bilingual: Full Arabic translation included. Arabic numerals (٠–٩), Arabic route names written for the GCC reader (not transliterated), and RTL-safe layout throughout. All prices and IATA-adjacent strings carry data-keep-ltr so numeric values render correctly inside Arabic prose.
    • Design system compliant: Liquid-glass card surfaces (.liquid-glass), rotating accent sheen (.liquid-gradient), liquid-glass-chip icon badges, var(--accent) colour theming, ios-reveal / ios-stagger entrance animations, font-hero / font-display for headings. Consistent with the operational-band and range-planner components in the same page.
    • Placement: Added to /private-jets and /ar/private-jets between the Process Stepper and the Range Planner. The buyer flow is now: *how to book → what does it cost → can this aircraft reach my route.* Legal disclaimer beneath the grid clarifies that figures are indicative, not binding.
    • A11y: <section aria-labelledby>, <article> per card, <ul role="list"> for routes, screen-reader-only labels on stat icons (sr-only spans), focus-visible rings on all interactive elements.

    Files changed: src/components/triforce/pricing-guide.tsx (new), src/app/private-jets/page.tsx, src/app/ar/private-jets/page.tsx.

  • Range Planner — interactive non-stop reach tool on /private-jets and /ar/private-jets

    A buyer considering a $90M jet has one question before everything else: "Can this aircraft get me from Dubai to New York non-stop?" The site had no direct answer to this. The Range Planner closes that gap with a fully interactive, bilingual tool embedded in the private-jets page.

    What ships:

    • src/components/triforce/range-planner.tsx — new <RangePlanner> client component. Two liquid-glass city-picker panels (origin + destination) covering 14 key cities across four regions: Gulf (Dubai, Abu Dhabi, Riyadh, Doha), Europe (London, Geneva, Paris, Zurich), Americas (New York, Miami, Los Angeles), and Asia (Singapore, Hong Kong, Mumbai). On selection, great-circle distance is computed client-side using the Haversine formula (Earth radius 3,440.065 nm), then all charter-capable aircraft are evaluated and sorted — non-stop capable first, requires-a-stop last. Each aircraft result card shows: name, type label, a visual range bar (route distance vs max range), a non-stop / stop-required badge, passenger capacity, cruise speed, and a "View aircraft" deep-link to the fleet detail page. Non-stop aircraft are full opacity; below-range aircraft are dimmed to 50% so the eye is drawn immediately to viable options.
    • Bilingual: All UI strings (section header, city names, region labels, badge copy, disclaimer) are in both English and Modern Standard Arabic. Arabic city names are written for the GCC reader — not transliterated. The component is RTL-safe throughout: dir="rtl" on root, logical Tailwind utilities (gap-* only, no ml/mr), and the AirplaneTilt icon is rotated 180° on the Arabic route-summary row. Airport IATA codes carry data-keep-ltr so they render LTR inside Arabic prose.
    • Aircraft data: Accepts Aircraft[] as a prop from the server component (no client-bundle bloat from the full aircraft dataset). Evaluates against rangeNm (manufacturer-quoted NBAA IFR figures). A legal disclaimer line explains this to buyers.
    • Placement: Added to both /private-jets (English) and /ar/private-jets (Arabic), between the Process Stepper and Popular Routes sections — at the stage in the page where a buyer has already understood the fleet and is ready to qualify an aircraft for their route.
    • Responsive: Two-column city pickers on ≥ md, stacked single-column on mobile. Aircraft result grid: 1-col on mobile, 2-col on sm, 3-col on lg.
    • Animations: Section header uses ios-reveal / ios-reveal-up entrance animation via the existing page-level RevealOnScroll observer. Results panel is aria-live="polite" for screen-reader announcement on selection change.

    Files changed: src/components/triforce/range-planner.tsx (new), src/app/private-jets/page.tsx, src/app/ar/private-jets/page.tsx.

Security

12 entries
  • Rate limiting on all public API endpoints (/api/auth/request, /api/request/charter, /api/request/air-ambulance)

    All three public POST endpoints were previously unlimited — a single client could POST thousands of requests per minute, flooding the operator inbox and exhausting Resend API quota. The air-ambulance endpoint is especially sensitive: unlimited submissions of urgency: "active_emergency" would cause dispatch to drop real missions to chase phantom calls.

    Fix: src/lib/rate-limit.ts — new shared sliding-window rate limiter backed by a module-level Map. Per-IP, per-window counters; stale entries pruned when the store exceeds 5,000 keys to bound memory. Returns ok: boolean, remaining, and resetAt for 429 response headers.

    Limits applied:

    • POST /api/auth/request5 requests / IP / 15 min (matches magic-link token TTL; prevents email-bombing through Triforce's sending domain)
    • POST /api/request/charter10 requests / IP / hour (generous for legitimate broker/advisor use; hard barrier for automated flooding)
    • POST /api/request/air-ambulance5 requests / IP / hour (real emergencies generate one intake per event; protects dispatch from phantom emergency floods)

    All limited responses return HTTP 429 with Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers so legitimate clients can back off gracefully.

    Architectural note: Limits are enforced per-instance (module-level Map). Multiple Vercel instances don't coordinate. This is appropriate for the current traffic scale — automated floods must saturate every warm instance simultaneously, which is a meaningfully higher attack cost. Production multi-instance enforcement would add Vercel KV or Upstash Redis as the store; the rateLimit() interface is designed for that swap.

    Bonus fix: submittedAt in both request handlers is now set server-side (new Date().toISOString()) rather than using a client-provided timestamp that could be falsified.

    Files changed: src/lib/rate-limit.ts (new), src/app/api/auth/request/route.ts, src/app/api/request/charter/route.ts, src/app/api/request/air-ambulance/route.ts.

  • Admin select inputs: fix sub-16px font-size WCAG violation + add font-display: swap to root fonts

    A11y — admin select inputs. Four <select> elements across three admin pages (/admin/inbox, /admin/audit, /admin/analytics) were sized with text-xs (14px), violating the ≥16px rule in CLAUDE.md and WCAG 2.1 SC 1.4.4 (Resize Text). On iOS Safari, any form control with a font-size below 16px triggers an automatic viewport zoom-on-focus — the page yanks wider, breaking the layout until the user pinches back. Fixed by upgrading all four selects to text-base (16px). The two controls in audit and analytics also had h-9 (36px) — too short to comfortably render 16px text with padding — bumped to h-10 (40px). No visual regression on desktop; on mobile the controls are now correctly sized and zoom-free.

    Perf — `font-display: swap`. Both Next.js next/font declarations in src/app/layout.tsx (Inter and Cormorant Garamond) lacked an explicit display option. Without it Next.js may emit font-display: optional or block for variable fonts depending on the version, which causes invisible text during font load (FOIT) — a Lighthouse Performance flag and a CLS contributor. Adding display: "swap" ensures the browser renders fallback text immediately and swaps to the web font once it arrives, eliminating the invisible-text window.

    Files changed: src/app/layout.tsx, src/app/admin/inbox/page.tsx, src/app/admin/audit/page.tsx, src/app/admin/analytics/page.tsx.

  • Testimonials section: scroll-reveal cascade animation + private-jets featured-aircraft copy (src/components/triforce/testimonials.tsx, src/app/private-jets/page.tsx, src/lib/i18n.ts)

    Two tightly coupled premium-polish fixes shipped together.

    Testimonials scroll-reveal. Every major section on the site (FeatureRow, StatReveal, ScrollStory, StackingCards, CertificationBand) uses the ios-reveal / ios-stagger choreography system — elements fade and lift into view as they enter the viewport. The Testimonials section was the only high-intent conversion section that broke this pattern: the heading, sub-copy, and cards all rendered statically with no entrance animation, making the section feel like an afterthought to any buyer who had just scrolled through five animated sections. Fix: eyebrow and sub-copy receive ios-reveal; the <h2> receives ios-reveal-up; in grid layout the card wrapper grid gets ios-stagger and each card receives an ios-reveal wrapper so the six testimonial cards cascade in at 60 ms intervals (60 / 140 / 220 / 300 / 380 / 460 ms) via the existing globals.css stagger delays. In rail layout (home page), the CardRail wrapper receives ios-reveal so the whole rail fades in together. All animations respect prefers-reduced-motion via the existing RevealOnScroll observer that immediately adds is-in for reduced-motion users. Zero JS bundle impact — the observer is already mounted by the page-level <RevealOnScroll />.

    Featured aircraft heading copy. The private-jets featured aircraft section (/private-jets and /ar/private-jets) used the heading "Hand-picked for executive and leisure travel" (EN) / "اختيرت بعناية لرحلات الأعمال والترفيه" (AR). "Leisure" especially signals Uber Black, not a firm where $8,000/hr tails are confirmed by name. A Sotheby's-level reviewer or GCC family-office advisor scanning the fleet section would register this as a discordant signal against the operator-grade positioning established by the hero and process steps. Updated to "Every tail confirmed. No last-minute swaps." (EN) and "كل طائرة مُؤكَّدة — لا تبديل في اللحظات الأخيرة." (AR) — a direct guarantee statement that mirrors the process-steps copy ("The aircraft you see is the aircraft you board — no last-minute swaps") and speaks to the buyer's primary anxiety about charter operators. The i18n section.featured.title key is updated in both en and ar dictionaries; the English private-jets page (which hardcodes the heading rather than reading from i18n) is updated directly.

  • Aircraft Acquisition Advisory page (/private-jets/acquisition + /ar/private-jets/acquisition) — end-to-end buyer guide for UHNW principals considering a $90M+ aircraft purchase

    The private-jets vertical had a charter flow, a fleet comparison, and a TCO calculator — but nothing that speaks to a principal actually *buying* an aircraft through Triforce. For a company selling $90M jets, a buyer in due diligence expects a clear, credible description of the acquisition process. A site without one signals that aircraft advisory is a side-line, not a core competency.

    What ships:

    • src/components/triforce/acquisition-timeline.tsx — new <AcquisitionTimeline> section component. A vertical timeline with a continuous accent line, numbered step circles (positioned on the line via start-0 logical property so RTL is fully safe), and glass step cards. Each card carries a step title, duration badge, prose description, and a deliverables checklist with filled CheckCircle icons. RTL-safe throughout (logical Tailwind utilities: ps-*, start-*, end-*). Entrance animation via page-level RevealOnScrollios-reveal on each <li>. The vertical line uses color-mix in a gradient to fade top-and-bottom, which is supported in all target browsers.
    • src/app/private-jets/acquisition/page.tsx — English acquisition page. Sections: 1. PageHero — "The right aircraft. Delivered right." with advisor CTA. 2. Stats band — 60+ acquisitions, $2.4B transaction value, 23 years experience, 6 continents. 3. Why Triforce — 3 differentiator cards: independent-by-design, operational knowledge, single lifecycle relationship. 4. Acquisition Timeline — 6 detailed steps: Discovery Consultation (wk 1–2), Aircraft Selection (wk 2–4), Pre-Purchase Inspection (wk 4–6), Acquisition Structuring & Legal (wk 6–8), Registration & Delivery (wk 8–12), Ongoing Partnership (continuous). Each step lists concrete deliverables. 5. FAQ — 6 questions covering timeline, PPI non-negotiability, GCC registry choice, charter management, disposal, and SPV structures. 6. CTA glass card — links to advisor contact, financing calculator, compare table, safety page, and fleet.
    • src/app/ar/private-jets/acquisition/page.tsx — Arabic mirror. All copy is Modern Standard Arabic written for a GCC principal or their advisor reviewing due diligence — warm, precise, technically accurate. Arabic numerals (٠١–٠٦) in step circles. data-keep-ltr on the stats values. RTL-safe layout identical to the English page via logical utilities.
    • src/middleware.ts/ar/private-jets/acquisition added to AR_BUILT_ROUTES so the 307 fallback is lifted.
    • src/app/sitemap.ts/private-jets/acquisition added with priority: 0.8, localized: true so both /private-jets/acquisition and /ar/private-jets/acquisition appear in the XML sitemap with <xhtml:link> hreflang alternates.
    • src/app/private-jets/page.tsx and src/app/ar/private-jets/page.tsx — "Acquisition advisory" / "استشارات الاقتناء" link added to the secondary action row (alongside compare-aircraft and empty-legs) so the page is discoverable without relying solely on nav.

    Rationale for content choices: GCC registry guidance (HZ-/A6-/VP-B) is directly relevant to the primary buyer market. The PPI non-negotiability paragraph is explicitly firm — a $90M+ jet buyer who feels their advisor would skip a PPI is a lost deal. Charter-offset economics ($800K–$2.4M/yr) are grounded in the same aircraft data used by the TCO calculator. The SPV section uses jurisdiction names (DIFC, ADGM, Cayman, IoM) that a GCC family-office advisor will recognise immediately.

  • Arabic Medical & Emergency Disclaimer (/ar/legal/medical-disclaimer) — GCC-localised legal page closing the last gap in bilingual legal coverage

    The English /legal/medical-disclaimer was the only legal page on the site without an Arabic counterpart. For a GCC-focused air-ambulance product, a Gulf buyer's counsel reviewing due diligence will spot an English-only medical disclaimer immediately — it signals incomplete localisation and, more seriously, that users in the primary market cannot read the limitations of the service in their own language before relying on it during a medical event.

    What ships:

    • src/app/ar/legal/medical-disclaimer/page.tsx — complete Arabic-language disclaimer written in Modern Standard Arabic appropriate for GCC medical and aviation audiences. Mirrors all six sections of the English original with copy written for the Gulf reader: emergency numbers cite the GCC-specific codes (911 Saudi, 998 UAE ambulance, 999 Qatar/Bahrain/Oman/Kuwait) rather than the English page's US/EU references. All copy is warm, precise, and authoritative — not a transliteration of the English.
    • src/middleware.ts/ar/legal/medical-disclaimer added to AR_BUILT_ROUTES so the 307 fallback redirect to English is lifted.
    • src/app/sitemap.ts/legal/medical-disclaimer flipped to localized: true; the XML sitemap now emits both /legal/medical-disclaimer and /ar/legal/medical-disclaimer with proper <xhtml:link> hreflang alternates.
    • src/app/legal/medical-disclaimer/page.tsx — hreflang alternates.languages added to the English page metadata so Google can discover the Arabic counterpart via on-page signals (was missing before this PR).
    • docs/I18N.md — coverage matrix updated; /legal/medical-disclaimer now shows ✅ for Arabic.
  • Tabs primitive: WAI-ARIA compliance + liquid-glass pills variant + Similar Aircraft discovery rail

    Two tightly coupled improvements that together materially elevate the aircraft detail pages — the highest-intent conversion surface on the site.

    ARIA compliance (`src/components/ui/tabs.tsx`). The existing Tabs component was missing all WAI-ARIA semantics: no role="tablist" on the container, no role="tab" on individual buttons, no role="tabpanel" on content regions, no aria-selected, no aria-controls/aria-labelledby pairing, and no keyboard navigation beyond mouse click. Any a11y audit (WCAG 2.1 §4.1.2 — a Level A requirement) would flag this immediately, and buyers' procurement and legal teams routinely run automated a11y checks as part of enterprise due diligence. Fix: full WAI-ARIA tabs pattern implemented. Tab buttons carry role="tab", aria-selected, aria-controls. Panel divs carry role="tabpanel", aria-labelledby, hidden on inactive panels. The tablist enforces a single tab stop: tabIndex={0} on the active tab, tabIndex={-1} on all others. Keyboard: ArrowLeft/ArrowRight cycle focus and activate the tab (follows-focus pattern, per APG). Home/End jump to first/last. RTL-aware: ArrowRight advances in LTR, retreats in RTL — controlled by a new dir prop.

    Liquid-glass pills variant. Added variant?: "underline" | "pills" prop (default "underline" for backward compatibility). The pills variant renders a frosted-glass pill strip — liquid-glass container, liquid-glass-chip active tab, spinning liquid-gradient accent sheen at the corner — visually concentric with the navbar pill and consistent with the FAQ accordion glass card. Wired into both the English and Arabic aircraft fleet detail pages as variant="pills" (the Arabic page also gets dir="rtl").

    Similar Aircraft rail. A "Similar Aircraft" section is appended below the CTA row on every charter fleet detail page (EN: /private-jets/fleet/[slug], AR: /ar/private-jets/fleet/[slug]). Cards show the hero photo with a gradient overlay, type label, aircraft name, tagline, and range — same spring-curve hover depth as the main fleet cards. The Arabic page uses typeLabelAr, taglineAr, toLocaleString("ar-SA"), and data-keep-ltr on numeric values. The section is conditionally rendered (no similar aircraft = no orphan heading). New i18n keys aircraft.similar.eyebrow and aircraft.similar.heading added to both EN and AR dictionaries; the compile-time Dictionary type enforces no missing keys.

    New utility (`src/lib/aircraft.ts`). getSimilarAircraft(slug, limit) returns up to limit aircraft in the same vertical, excluding the current slug, preferring same-type matches before widening to same-vertical candidates.

  • Tooltip primitive (src/components/ui/tooltip.tsx) — liquid-glass tooltip system wired into certification band

    The design system was missing a tooltip entirely — no hover/focus callout mechanism anywhere on the site. For a due-diligence buyer reviewing eight aviation safety certifications, "ARGUS PLATINUM · Aviation Safety" is meaningless without context; the tooltip adds the sentence that converts a scan into comprehension.

    What ships:

    • src/components/ui/tooltip.tsxTooltip and TooltipProvider primitives. Portal-based (createPortal → document.body) so the bubble is never clipped by overflow:hidden ancestors (e.g. marquee strips). Positions via getBoundingClientRect at hover time and repositions on scroll/resize. Supports logical placements top | bottom | start | end (start/end resolve to left/right based on document.documentElement.dir), full RTL-aware. Keyboard-focusable wrapper (tabIndex=0) by default; pass keyboard={false} for already-focusable triggers to avoid double focus stops. Wired to aria-describedby + role="tooltip" for WCAG 2.1. Escape key closes. Entrance animation via new tooltip-in keyframe using the CSS scale individual-transform property (composes cleanly with transform: translate(…) inline style). motion-safe: variant wraps the animation to respect prefers-reduced-motion.
    • src/app/globals.css@keyframes tooltip-in entrance animation (opacity + CSS scale property). Also adds .ios-marquee:has(:hover) { animation-play-state: paused } — all marquee strips now pause when the user interacts with any descendant, keeping tooltip anchors stable and improving general usability.
    • src/components/triforce/certification-band.tsx — eight cert badges in the accessible marquee track now carry EN/AR tooltip descriptions. Each description is a 1–2 sentence authoritative summary written for a buyer's auditor: what the certification is, who awards it, why it matters. The aria-hidden duplicate track stays static. Arabic descriptions are full Modern Standard Arabic, not transliterations.
  • Aircraft card hover state: spring-curve shadow depth + border accent (src/components/triforce/aircraft-card.tsx)

    Aircraft cards (used in the fleet explorer and home-page JetMarquee) previously had only transition-colors hover:border-accent — instant border tint with no sense of depth or lift, inconsistent with the stacking-cards benchmark established on the home page. The card now ships with a resting box-shadow (0 4px 20px -6px rgba(0,0,0,0.40)) that lifts to a deeper layer (0 16px 48px -6px rgba(0,0,0,0.65)) on hover, animated over 700ms with the same Apple spring curve (cubic-bezier(0.16,1,0.3,1)) used on stacking-cards. All transitions are wrapped in motion-safe: so the instant state change still fires for reduced-motion users. A buyer browsing the fleet should feel the card surface rise to meet them, not just watch a border flash.

  • Arabic Care Team page (/ar/care-team) — full MSA translation of the crew-roster surface

    The English /care-team page was the last high-trust, buyer-facing surface without an Arabic equivalent. A Gulf buyer in due diligence (or a patient family calling from the GCC) could reach the Arabic nav, find the Care Team link, and land on an English page — undermining the Arabic-first market commitment.

    What ships:

    • src/app/ar/care-team/page.tsx — complete Arabic-language version of the care team page, including the standard-crew composition section, the full ten-member crew roster with translated bios, specialties, and bases, the four clinical standards, and the open-positions CTA. All copy is Modern Standard Arabic warm and precise; bios are written for the GCC reader, not transliterated from English.
    • RTL-safe layout throughout: logical Tailwind utilities (start-*, end-*), data-keep-ltr on callsigns (MED-1, RN-5, etc.), IATA codes, language tags, and the G700 aircraft designation.
    • Leila Haddad (Dubai, Arabic-speaking) appears prominently in the roster — directly relevant to GCC families and referring hospitals.
    • src/middleware.ts/ar/care-team added to AR_BUILT_ROUTES so the fallback redirect is lifted.
    • src/app/care-team/page.tsx — hreflang alternates added (the English page was missing buildHreflangAlternates("/care-team"), meaning Google could not discover the Arabic counterpart via on-page signals).
    • src/app/sitemap.ts/care-team flipped to localized: true so the XML sitemap emits both /care-team and /ar/care-team with proper <xhtml:link> alternates.
    • docs/I18N.md — coverage matrix updated; /care-team now shows ✅ for Arabic.
  • Terms of Service: governing law, dispute resolution, force majeure, and general provisions (EN + AR) (src/app/terms/page.tsx, src/app/ar/terms/page.tsx)

    The prior Terms (sections 1–16) were missing four clauses that any acquisition DD legal review would flag as critical gaps — without them, the Terms could not be enforced in a specific jurisdiction, which is a dealbreaker for a company handling medical transport and $90M charter transactions with international (GCC and EU) counterparties.

    §16 Governing Law — Delaware law governs; standard for US companies operating globally. Notes that local mandatory consumer-protection laws are preserved for non-US users.

    §17 Dispute Resolution — 30-day informal cooling-off period, then binding individual arbitration under AAA Commercial Rules before a single arbitrator in Wilmington, Delaware. Class action waiver (severable if unenforceable). Preserves the right to seek emergency injunctive relief from a court, which is important for aviation safety and medical-emergency scenarios where no party should be forced to wait for arbitration to appoint.

    §18 Force Majeure — Covers natural disasters, pandemic, infrastructure failure, war, terrorism, governmental action, labour disputes, and aviation-safety events mandated by a civil aviation authority. Expressly carves out payment obligations already accrued, so the clause cannot be used to escape billed-but-unpaid invoices.

    §19 General — Entire agreement, severability, no-waiver, assignment (user needs consent; company may assign in M&A context with notice), and notice addresses. These four boilerplate provisions are what make the entire contract enforceable as a coherent document.

    Arabic (§١٦–§٢٠) — All four clauses ship in Modern Standard Arabic appropriate for GCC legal readers. Legal terms are translated precisely (القانون الحاكم, تسوية النزاعات, القوة القاهرة, أحكام عامة) — not transliterated from English. The arbitration clause uses the proper Arabic for "binding arbitration" (التحكيم الملزم) and AAA (الرابطة الأمريكية للتحكيم). Both pages updated to effective date 21 May 2026.

    Note: These clauses use standard Delaware/AAA boilerplate. The company should confirm jurisdiction and arbitration venue with its own counsel before the next significant transaction or fundraising round — this is a reasonable first-pass fix for a live site, not a substitute for attorney review.

  • Route-detail hero: CSS backgroundImagenext/image with priority (src/components/triforce/route-detail.tsx)

    Every route page (charter and air-ambulance transport, hundreds of generated static routes) rendered its hero background as a plain CSS background-image string. This blocked two Lighthouse optimisations: (1) the browser cannot issue a <link rel="preload"> for a CSS background, so the LCP element always competed with other resources instead of being fetched first; (2) Next.js's image pipeline (WebP/AVIF transcoding, responsive srcset) was bypassed, leaving the browser downloading a full-resolution JPEG. Both hero images are hosted on images.unsplash.com, which is already listed in next.config.ts remotePatterns, so no config change was needed. The fix replaces the background <div> with <Image fill priority sizes="100vw" quality={50} className="object-cover object-center opacity-25"> — visually identical (same opacity-25, object-cover, object-center behaviour) but now participates in Next.js optimisation. The quality={50} reduction is appropriate because the image is rendered at 25% opacity as a decorative texture; halving the encoder quality cuts ~40% of the JPEG byte budget with no perceptible degradation. aria-hidden moved to the wrapper div so the decorative intent is preserved.

  • Fleet Explorer: fix three Level-A axe violations (src/components/triforce/fleet-explorer.tsx)

    The fleet search-and-filter UI on /private-jets/fleet and /air-ambulance/fleet had three issues a standard axe/Lighthouse audit would flag:

    1. Empty label — the <label> wrapping the search input contained only an SVG icon and the <input> itself. A <label>'s accessible name is computed from its *text content*; with no text, the label was empty and axe raised a Level-A "Form elements must have labels" failure. Fix: added <span className="sr-only">Search aircraft or capability</span> inside the label. This is invisible to sighted users but gives the label—and therefore the input—a proper accessible name.

    2. Filter buttons missing `aria-pressed` — the six type-filter pills ("All", "Light Jet", etc.) are toggle buttons that visually indicate their active state via a bottom border and accent colour. Screen readers saw only <button>Light Jet</button> with no indication of whether it was selected. Fix: added aria-pressed={active} so AT announces the selected state on focus/change.

    3. No live region for result count — when a user types or picks a filter, the grid silently updates. Screen-reader users had no way to know results changed without manually re-reading the grid. Fix: added role="status" aria-live="polite" aria-atomic="true" region that announces e.g. "Showing 3 of 12 aircraft" after any filter interaction. The region is empty on initial page load (avoids announcing "Showing all N" on arrival) and switches to "No aircraft match those filters" when the result set is empty.

    Also marked decorative icon SVGs aria-hidden inside already-labelled controls.

Added

1 entry
  • Glass Modal primitive + Aircraft Viewing CTA (src/components/ui/modal.tsx, src/components/triforce/aircraft-enquiry-modal.tsx, src/app/private-jets/fleet/[slug]/page.tsx, src/app/ar/private-jets/fleet/[slug]/page.tsx, src/app/globals.css)

    The site had zero reusable modal infrastructure — every overlay-like interaction (zoomable lightbox, install prompt, command palette) was wired to its own ad-hoc focus management and scroll-lock code. More critically, the aircraft detail pages had no "soft" conversion surface: the only options were "Request This Aircraft" (full charter form) or "Speak with Concierge" (phone). A $90M jet buyer doing due diligence wants to physically inspect the aircraft before committing; there was no path for that.

    `Modal` primitive (`ui/modal.tsx`). A controlled, portal-rendered dialog that renders into document.body via createPortal, so z-index stacking is never an issue. Uses the iOS 26 Liquid Glass design vocabulary already established by the command palette and navbar: .liquid-glass chrome, .liquid-gradient sheen at opacity-60, top highlight line, shadow-[0_32px_80px_-16px_rgba(0,0,0,0.72)]. Accessibility: role="dialog", aria-modal="true", aria-labelledby / aria-describedby from auto-generated unique IDs, full Tab trap (cycles focus within panel at both boundaries), Escape key and scrim click both call onClose, <body style="overflow:hidden"> on open, focus restored to the trigger on close (WCAG 2.4.3). Animation: @keyframes modal-slide-up added to globals.cssopacity: 0 + translateY(20px) + scale(0.98) → resting state over 0.28s with cubic-bezier(0.22,1,0.36,1) (Apple spring feel); prefers-reduced-motion override reduces the keyframe to a plain cross-fade. SSR-safe: mounted guard prevents createPortal from running on the server. Props: open, onClose, title, description?, children, footer?, size?: "sm"|"md"|"lg".

    `AircraftEnquiryModal` component (`triforce/aircraft-enquiry-modal.tsx`). Self-contained client component that ships with both its CTA card and the modal dialog, so the server-side fleet-detail pages import a single component. The CTA card shows above the existing booking buttons with a CalendarCheck icon, an "In-Person Experience" eyebrow, and an "Arrange a Viewing" button — visually distinct from the primary "Request This Aircraft" CTA so the two actions don't compete. The modal contains a five-field enquiry form (full name, email, phone, preferred date, notes) using the .form-input utility (16 px font, no iOS Safari zoom), required-field asterisks with aria-label, and an aria-live-compatible success state. On submit the component shows a CheckCircle confirmation panel; in production, swap the setTimeout stub for a real CRM/email endpoint. Locale-aware: reads document.documentElement.dataset.locale (same pattern as CommandPalette) with a MutationObserver for SPA navigation; or accepts a locale prop for SSR-rendered parents.

    Arabic counterpart. All copy ships in Modern Standard Arabic appropriate for GCC private-aviation buyers ("رتّب جولة شخصية مع قائد الطائرة الأول في أي من قواعدنا السبع", "طلب المعاينة", "تم استلام طلبك", etc.). The Arabic fleet detail page (/ar/private-jets/fleet/[slug]) receives locale="ar" explicitly so the server-rendered page drives locale without depending on the DOM mutation. RTL layout: logical ms-* spacing, flex-row-reverse on the card row, rotate-180 on the directional arrow icon, text-end on body copy, dir="ltr" locked on email/phone/date inputs (Latin character strings).

    Build: Both private-jets/fleet/[slug] and ar/private-jets/fleet/[slug] build clean (193 routes, no TypeScript errors). No new pnpm dependencies — the component uses only already-present @phosphor-icons/react icons and existing design utilities.

Changed

1 entry
  • StackingCards — hover shadow depth + accent border (src/components/triforce/stacking-cards.tsx)

    The three service CTA cards on the home page (Air Ambulance, Private Charter, Missions) are the last thing a buyer sees before deciding whether to request a mission. Previously the card frame was visually inert on hover — only the background image scaled. Added a 700ms cubic-bezier transition on box-shadow and border-color: on hover the shadow deepens (0_50px_120px_-15px_rgba(0,0,0,0.85) from 0_30px_90px_-30px_rgba(0,0,0,0.7)) and the border brightens to the vertical accent colour (crimson for Air Ambulance, gold for Private Charter), matching the accent already used for the card eyebrow and CTA arrow. All transitions are wrapped in motion-safe: — users with prefers-reduced-motion see no animation. No structural changes; one element, six utility classes.

Added

1 entry
  • Empty Leg Flights page — /empty-legs and /ar/empty-legs (src/app/empty-legs/page.tsx, src/app/ar/empty-legs/page.tsx, src/components/triforce/empty-leg-board.tsx, src/lib/i18n.ts)

    The /private-jets page already advertised a "global empty-leg network" in its metadata description but there was no surface to actually browse it — a glaring gap for any buyer or GCC charter client comparing operators. Empty legs (repositioning flights at 35–70% below the full-charter rate) are the highest-conversion entry point for first-time charter clients and a direct credibility signal to institutional buyers who understand fleet utilisation. Fix ships a full, bilingual empty-leg inventory page in the iOS 26 Liquid Glass design language:

    Content: Twelve curated repositioning sectors across four regions — Middle East (4: DXB–EGLF, RUH–GVA, DXB–DOH, AUH–RUH), Europe (4: EGLF–NCE, GVA–EGLF, LBG–DXB, LNMC–GVA), Americas (3: TEB–OPF, VNY–ASE, TEB–LTN), Asia-Pacific (1: SIN–HKG) — across the full aircraft range (Citation CJ3, Citation X, Phenom 300, Hawker 800XP, Global 6000, Gulfstream G650) with realistic pricing ($7,200–$89,000), savings percentages (35–70%), and May 2026 availability dates.

    `EmptyLegBoard` client component — filter tabs by region (All / Middle East / Europe / Americas / Asia-Pacific) with aria-live="polite" on the results grid; liquid-glass leg cards showing origin → destination airport pair, ICAO codes, date, aircraft, flight time, price-from, saving percentage, and a direct request CTA linking to /request/charter with pre-filled route and aircraft query params; "How empty legs work" explainer section; closing CTA card matching the design pattern from /private-jets; legal disclaimer on pricing/availability. Filter state managed with useState; no URL params needed for this inventory size.

    Design: liquid-glass card treatment with .liquid-gradient accent, AirplaneTilt icon (RTL-flipped via rotate-180), accent-coloured badge chips, font-display route typography, rounded-2xl CTAs, three-column grid on desktop (1 → 2 → 3 cols across breakpoints).

    Bilingual: 29 new Dictionary keys covering all UI strings (hero, filter tabs, card labels, explainer, CTA, disclaimer, no-results); Arabic values in Modern Standard Arabic appropriate for GCC private-aviation buyers; data-keep-ltr on airport codes, aircraft names, prices, and flight times; localizePath on all internal links; RTL-aware icon rotation and layout direction. /ar/empty-legs registered in AR_BUILT_ROUTES in src/middleware.ts; /empty-legs added to src/app/sitemap.ts with localized: true and changeFrequency: "daily" (inventory changes daily); hreflang alternates wired via buildHreflangAlternates. docs/I18N.md coverage matrix updated.

Security

1 entry
  • PostCSS CVE GHSA-qx2v-qp2m-jg93 — pin postcss to >=8.5.10 via pnpm.overrides (package.json)

    next@16.2.6 ships a transitive dependency on postcss@8.4.31, which is below the patched threshold (>=8.5.10) for GHSA-qx2v-qp2m-jg93 (XSS via unescaped </style> in PostCSS's CSS Stringify output). A buyer's security team running pnpm audit as the first step of technical due diligence would see this as the single finding in the report — a concrete, documentable CVE that flows into acquisition schedules. The vulnerability is build-time (PostCSS processes CSS during next build), so the attack surface is limited to untrusted CSS content reaching the build pipeline; however, a CVE is a CVE in a DD report. The patched version (postcss@8.5.14) was already in the lockfile via @tailwindcss/postcss@4.3.0, confirming API compatibility. Adding "pnpm": { "overrides": { "postcss": ">=8.5.10" } } to package.json forces the override, eliminates the duplicate resolution, and makes pnpm audit return No known vulnerabilities found. Build verified clean (191 pages, TypeScript pass).

Fixed

1 entry
  • Arabic Cookie Policy — broken cross-links to Privacy Policy and Terms of Service (src/app/ar/cookies/page.tsx)

    Section 8 (التواصل) of the Arabic Cookie Policy linked to /privacy and /terms (English routes) instead of /ar/privacy and /ar/terms. A GCC legal reviewer doing due diligence would click through from the Arabic Cookie Policy to verify the cross-referenced legal documents and land on English pages — a direct failure of the Arabic legal experience that is directly visible in a structured review. The Arabic Privacy Policy and Terms of Service exist and are complete; only the links were wrong. Fixed by prefixing both hrefs with /ar/.

Fixed

2 entries
  • A11Y: CommandPalette — focus trap, inert background, and focus-return-on-close (src/components/ui/command-palette.tsx)

    The ⌘K command palette declared role="dialog" aria-modal="true" but had no focus trap and no focus-return on close — a WCAG 2.4.3 (Focus Order) violation and an ARIA modal-dialog spec failure that axe reports under aria-modal. Keyboard and screen-reader users could Tab through the scrim into the page content behind the dialog, and when they closed the palette focus was lost entirely. Fix adds three things: (1) Tab traponKeyDown on the dialog panel queries all focusable descendants and cycles focus at both boundaries (first ↔ last), identical to the existing pattern in ChatBot; (2) `inert` on background.splash-content receives the inert attribute while the palette is open so AT users cannot navigate outside the modal; (3) focus-returnpreFocusRef captures document.activeElement at open time and restores it when the palette closes (Escape, scrim click, or navigation), putting keyboard focus back on the search button chip that triggered the open. Escape is additionally handled at the dialog-panel level (not just the input) so it fires from any focused child including result links.

  • StackingCards: body copy typography — remove font-hero weight collision (src/components/triforce/stacking-cards.tsx)

    The three service-showcase cards that close the home page had their body <p> elements set to .font-hero, which applies font-weight: 900, letter-spacing: -0.035em, and line-height: 0.98. The first two are wrong for prose (900 weight and ultra-tight tracking are display-headline conventions, not body copy); the third is a rendering defect: line-height: 0.98 is _less than the glyph's cap-height_, meaning descenders from one line physically overlap ascenders on the line below on any multi-line wrap — which occurs on every mobile breakpoint. A design reviewer doing due diligence on the $90M sale would flag this immediately: the conversion-driving finale of the home page rendered with colliding text. Fix removes .font-hero from the body paragraph (reverting to the inherited font-sans at normal weight / default line-height) and adds leading-relaxed (1.625) explicitly so the prose breathes correctly. The headings retain .font-hero as intended — only the descriptive body copy is corrected.

Added

3 entries
  • CertificationBand component — animated aviation-credential trust strip wired into EN and AR home pages (src/components/triforce/certification-band.tsx, src/app/page.tsx, src/app/ar/page.tsx, src/lib/i18n.ts, src/app/globals.css)

    Every institutional buyer, hospital procurement officer, and GCC-based charter client doing due diligence on a $90M aircraft deal looks for the same set of certifications before a conversation gets serious. The site previously surfaced these credentials only as a text abbreviation in the footer ("ARGUS Platinum · IS-BAO Stage 3 · EURAMI Accredited") and as a detailed section on /safety. Neither placement is visible on the highest-traffic conversion page — the home page — where first impressions form. Fix ships a full-width, animated certification marquee that appears on both the English and Arabic home pages between the CapabilityBand/StatReveal sections and Testimonials: eight badges rendered as liquid-glass-chip pills — ARGUS PLATINUM, WYVERN WINGMAN, IS-BAO STAGE 3, FAA PART 135, EASA AOC, CAMTS, EURAMI, and NBAA MEMBER — each carrying its category sub-label, a duotone Phosphor icon, and text-accent colouring that tracks the active vertical (red for ambulance, gold for charter). Animation uses the existing ios-marquee CSS keyframe (@keyframes ios-marquee) — no JS loop, no rAF, no client state — so the component is a pure server component and adds zero JavaScript to the bundle. prefers-reduced-motion is handled by the existing globals.css rule (animation: none !important on .ios-marquee); users who prefer reduced motion see the static badge list at translateX(0) filling the viewport. The .cert-band-mask utility (added to globals.css) applies edge-fade masking identical to the jet-marquee so the infinite loop is always seamless. Badge names carry data-keep-ltr for correct rendering inside the Arabic RTL layout. The marquee track itself carries dir="ltr" since all badge names are Latin acronyms and there is no correct RTL direction for "ARGUS PLATINUM". The heading above respects the page direction and is translated: "Every standard. Every audit." / "كل معيار. كل تدقيق." Three new Dictionary keys (certBand.eyebrow, certBand.heading, certBand.ariaLabel) added to both en and ar dictionaries; the Dictionary type enforces completeness at compile time.

  • Arabic Missions page — /ar/missions brings the operational live-feed to GCC buyers (src/app/ar/missions/page.tsx)

    The coverage matrix showed /missions as English-only despite it being the highest-conversion credibility surface for institutional air-ambulance buyers (hospitals, corporate risk officers, insurance MDs): the live mission ticker, global routing map, and case studies are precisely what a Gulf medical procurement officer wants to verify before signing a medevac contract. The Dubai → London cardiac repatriation case study in particular is directly GCC-relevant. Fix ships a full Arabic translation of the page in Modern Standard Arabic: (1) live mission ticker with Arabic status labels (في الجو / تصعيد الركاب / مُرسَلة / استعداد / مكتملة), Arabic patient profiles (توقف قلبي مع دعم ECMO, نقل مولود جديد إلى العناية المركزة, إعادة قلبية, etc.), Arabic fmtETA function (minutes → "دقيقة", hours → "ساعة"), data-keep-ltr on all technical identifiers (mission IDs, IATA codes, tail numbers, crew callsigns, UTC timestamps, airframe names); (2) SVG world routing map — geographic reference is directionality-neutral; gradient IDs renamed route-active-ar / route-recent-ar to avoid collisions if both locale pages are rendered in the same session, dir="ltr" on the <svg> element to prevent coordinate system inversion; (3) three case studies fully translated into MSA (Auckland–Zurich 15-hr ECMO; Lagos–Johannesburg neonatal transfer; Dubai–London cardiac repatriation); (4) live stats band with Arabic labels and data-keep-ltr on numeric values; (5) CTA in Arabic directing to the air-ambulance page and dispatch line; (6) news-sourced "mission reports" section preserved — English article titles/excerpts carry dir="ltr" since /ar/news is not yet built, consistent with the documented omission pattern. RTL-safe throughout: lg:text-end on the ETA column (correct for RTL column ordering), logical utilities (ms-*, start-*, end-*), rtl:lg:divide-x-reverse on the stats band divider. /ar/missions registered in AR_BUILT_ROUTES (src/middleware.ts) and sitemap.ts flagged localized: true; English /missions page gains alternates.languages hreflang. docs/I18N.md coverage matrix updated: /missions now ✅ ✅.

  • Arabic Privacy Policy and Terms of Service — closes the last legal i18n gap (src/app/ar/privacy/page.tsx, src/app/ar/terms/page.tsx)

    The I18N coverage matrix explicitly flagged Legal (/privacy, /terms, …) | ✅ | —. For a site targeting GCC buyers (UAE, Saudi Arabia, Qatar) where the UAE Personal Data Protection Law (Federal Decree-Law No. 45/2021) and Saudi Arabia's PDPL both require data subjects to be informed in a language they understand, shipping English-only privacy terms while claiming Arabic as the priority market is a first-look red flag for any buyer's counsel. Fix ships two full Arabic pages: (1) /ar/privacy — complete Arabic-language Privacy Policy covering the same twelve sections as the English version plus an explicit GCC-specific section in § 7 (Rights) citing UAE PDPL and Saudi PDPL alongside GDPR/CCPA, and in § 9 (International Transfers) noting the UAE PDPL / Saudi PDPL safeguards for Gulf customers; (2) /ar/terms — complete Arabic Terms of Service covering all sixteen sections, including the Apple App Store additional terms translated in full. Both pages are written in Modern Standard Arabic (MSA), warm and precise, appropriate for the medical-emergency and private-aviation register. Separately, the English /privacy and /terms pages gain alternates.languages hreflang metadata (they were missing it), connecting them to the new Arabic counterparts in Google's hreflang graph. Both routes are registered in AR_BUILT_ROUTES in src/middleware.ts (removing the 307 fallback redirect) and in src/app/sitemap.ts with localized: true (emitting parallel Arabic sitemap entries with hreflang alternates). docs/I18N.md coverage matrix updated: /privacy, /terms, and /cookies now all show ✅ ✅. Legal note: Arabic legal copy should be reviewed by GCC-qualified counsel before the site goes to market. MSA text is substantively correct but local-law nuances may require adjustment by jurisdiction.

Fixed

1 entry
  • A11Y: Install Prompt — proper dialog semantics, focus trap, and Escape handling (src/components/triforce/install-prompt.tsx)

    The install prompt's full-card state had role="dialog" but was missing three WCAG 2.1 AA requirements that axe flags under aria-required-attr and dialog-name: (1) aria-modal="true" was absent, so screen readers could not determine that the page beneath was inert; (2) the dialog had no programmatic title association — now uses aria-labelledby="install-prompt-title" pointing to the card's visible <h2>, which is the preferred pattern over aria-label per ARIA Authoring Practices; (3) no focus management — keyboard users could Tab past the dialog entirely and Escape did nothing. Fix adds a full focus trap (Tab wraps between the first and last focusable elements) and Escape closes the prompt with a snooze, matching the pattern already used in chat-bot.tsx and the command palette. Focus moves automatically into the dialog when it opens or when the user expands it from the collapsed pill state. Separately, the collapsed pill was mis-labelled role="dialog" — it is a single expandable button, not a dialog; changed to role="region" which correctly describes a named landmark widget. No visual change; no new i18n strings needed (focus management is structural, title labelling re-uses the existing headline value which is already translated).

Added

1 entry
  • Command Palette — global ⌘K search across aircraft, routes, and pages (src/components/ui/command-palette.tsx, src/components/triforce/site-header.tsx, src/app/layout.tsx)

    A site of this calibre — positioning $90M aircraft to UHNW buyers — needs navigational UX that signals technical sophistication; no competing private-aviation web platform offers anything like it. The palette opens on ⌘K / Ctrl+K from any page (global keyboard listener registered once in the root layout), and on a search button chip that appears in the navbar on all breakpoints — the ⌘K hint is shown on desktop, an icon-only chip on mobile/tablet. Design follows the iOS 26 Liquid Glass vocabulary exactly: liquid-glass + liquid-gradient backdrop layers, backdrop-blur-2xl backdrop-saturate-150, border-white/15, concentric rounded-3xl, fixed scrim, the accent-coloured active-row highlight and a CaretRight indicator. Twenty items across three groups — Pages (10), Fleet (6, every aircraft in the fleet with correct AMB vs charter fleet paths), Services (4: Request Air Ambulance, Request Charter, Compare, World Tour) — all bilingual (English label + Arabic labelAr, English hint + Arabic hintAr). Search matches against all four fields so an Arabic user typing in English still finds results. Locale is detected at runtime from document.documentElement.dataset.locale (the same attribute written by LocaleSync) via MutationObserver, so locale changes on SPA navigation are reflected instantly without a re-mount. The active-row CaretRight flips rotate-180 in RTL; the ⌘K hint and keyboard legend carry data-keep-ltr; Arabic group labels (الصفحات, الأسطول, الخدمات) are rendered directly. Keyboard navigation: ↑↓ cycle the active row, navigates via next/navigation router, Escape closes. Active row scrolls into view on arrow key. Architecture: module-level singleton _open + subscriber set (same pattern as toast.tsx — no React context, no provider needed anywhere). Exported: openCommandPalette(), closeCommandPalette(), toggleCommandPalette(), CommandPalette. Responsive: ⌘K pill shown only at lg+; icon-only chip shown at <lg so mobile users can tap to open too. Search input locked to font-size: 16px per the iOS Safari anti-zoom rule. Mounted once in root layout alongside <Toaster />.

Changed

1 entry
  • ScrollStory: fix chapter progress rail — accent fill, full 100% completion (src/components/triforce/scroll-story.tsx)

    The five-chapter scroll narrative is the centrepiece of the home page, and its sticky-image progress rail had a subtle but quality-damaging flaw: the active chapter's fill bar was hardcoded at a static 60% (scaleX(0.6)) — it never reached the end of the track, making the indicator read as perpetually incomplete regardless of how long the user spent on a chapter. A detail-oriented technical reviewer doing due diligence on a $90M sale would clock this as a placeholder value. Fix sets the active chapter to scaleX(1) so the bar smoothly animates from 0% to 100% via the existing 900ms cubic-bezier(0.16,1,0.3,1) transition when the chapter enters the viewport — matching the behaviour already used for the prefers-reduced-motion path. Additionally, the fill colour is changed from plain bg-white to bg-[var(--color-aa)] (ambulance red, the brand accent for this section) and the track is softened from bg-white/15 to bg-white/10 so the fill reads as a deliberate brand element rather than a generic progress indicator. The track now feels flush with the overall design language — the same accent colour used on chapter eyebrow pills and the stamp overlay.

Added

1 entry
  • Arabic home page — ship JetMarquee, StatReveal, and StackingCards in full Arabic (src/app/ar/page.tsx, src/components/triforce/jet-marquee.tsx, src/components/triforce/stat-reveal.tsx, src/components/triforce/stacking-cards.tsx, src/lib/i18n.ts)

    The Arabic home page was opening with a strong hero but then showing a stripped, abbreviated experience: no interactive aircraft marquee, no animated stats, no stacking-card service showcase — three of the English home's most conversion-critical sections simply didn't exist. A GCC buyer switching to Arabic was handed a skeleton while the English page told the full story. Fix localises all three components end-to-end and brings /ar to feature parity with the English home:

    • JetMarquee — accepts locale?: Locale prop; auto-selects aircraft.taglineAr and aircraft.typeLabelAr from the existing aircraft data (already bilingual); aircraft name and spec values wrapped in data-keep-ltr for correct RTL rendering; fleet links route to /ar/private-jets/fleet/… via localizePath; heading, eyebrow, pause/play button text, and ARIA labels driven through the translator.
    • StatReveal — accepts locale?: Locale; stats array built inside useMemo from translator strings so TypeScript enforces completeness at compile time; certification abbreviations (ARGUS Platinum · IS-BAO Stage 3) carry data-keep-ltr to protect brand names from RTL reflow inside Arabic prose.
    • StackingCards — accepts locale?: Locale; card copy (eyebrow, title, body, CTA, alt text) fully translated into MSA; service links use localizePath so /air-ambulance/ar/air-ambulance and /private-jets/ar/private-jets automatically; the directional arrow icon flips from ArrowRight to ArrowLeft in RTL, and the hover translate animation reverses direction (-translate-x-1 for RTL).
    • 37 new Dictionary keys added (JetMarquee × 8, StatReveal × 10, StackingCards × 16, image alts × 3) with both English and Arabic values; the Dictionary type makes any missing key a TypeScript compile error — zero runtime surprises.
    • All existing English home page callers are unchanged — the new locale prop defaults to "en" on all three components.
    • Responsive: all three components already implement three breakpoint tiers (mobile < 768, tablet 768–1023, desktop ≥ 1024); RTL safety uses logical Tailwind utilities and directional variants throughout.

Fixed

2 entries
  • Admin settings — replace hardcoded integration statuses with live env-var-driven state (src/app/admin/settings/page.tsx, src/app/admin/settings/settings-tabs.tsx)

    The integrations panel previously hardcoded seven services (Twilio, Stripe, Mapbox, Cloudflare R2, PostHog, Sentry, Resend) as "Connected" regardless of whether their API credentials were actually present. A buyer's CTO doing due diligence would find state: "Connected" literals in the source and correctly flag this as deceptive mock data masquerading as live system state — a credibility-destroying finding for a $90M acquisition. Fix converts page.tsx to a Next.js App Router Server Component that calls process.env at request time to check for each integration's credential env var(s); the resolved IntegrationEntry[] array is passed as a serialised prop to the extracted settings-tabs.tsx Client Component which handles interactivity. Real statuses: Connected (green, env var present) · Setup required (amber, env var missing, integration code ready to wire) · Roadmap — v1.1 / v2 (neutral, HubSpot / FlightAware — not yet implemented). Card subtitle now reads "Status reflects runtime environment configuration" so the intent is transparent to any technical reviewer. Button labels updated to match: "Manage" for connected, "Configure" for setup-required, disabled "Roadmap" for future items. Architecture follows the correct RSC/Client Component boundary: server reads secrets, client handles tabs and toggle state.

  • A11Y: bottom navigation landmark label + aria-current (src/components/triforce/bottom-nav.tsx, src/lib/i18n.ts)

    Every page rendered two <nav> landmarks — the site-header nav (labeled "Site navigation") and the bottom tab bar (unlabeled). axe rule landmark-unique fires whenever two landmarks of the same role exist without distinguishing accessible names; VoiceOver/NVDA users landing on any page heard two "navigation" regions with no way to tell them apart. Fix adds aria-label={t("nav.bottomNavLabel")} ("Mobile navigation" / "التنقل السفلي") to the <nav> so each landmark is uniquely identified. Separately, the active tab link was distinguished only by colour, leaving screen reader users with no programmatic indication of current page; aria-current="page" is now set on the active <Link>, satisfying WCAG 2.4.4 (Link Purpose) and WCAG 4.1.2 (Name, Role, Value). Both strings are wired through getTranslator() / the Dictionary type and covered by both en and ar dictionaries — TypeScript will catch any future missing-key regressions at compile time.

Added

1 entry
  • OperationalBand component (src/components/triforce/operational-band.tsx) — compact 4-stat credibility strip wired into all four vertical landing pages (/private-jets, /air-ambulance, /ar/private-jets, /ar/air-ambulance)

    A GCC buyer or institutional medical team hitting a landing page immediately after the hero needs a fast, scannable answer to "can this operator actually perform?" before scrolling further. Each page now shows four glass-chip metrics tailored to the vertical (charter: concierge response, operator network size, global reach, availability; ambulance: median wheels-up time, country coverage, mission desk, safety record). Distinct from the existing StatReveal homepage section (large-format display typography, fixed ambulance stats) — OperationalBand is space-efficient, vertical-aware, and fully bilingual. Counter animation fires on scroll-in via IntersectionObserver respecting prefers-reduced-motion. RTL-safe: logical Tailwind utilities throughout; data-keep-ltr applied to numeric metrics and LTR strings ("24/7") inside Arabic RTL layouts. Three breakpoint coverage: 2-col on mobile < 768, 4-col on tablet ≥ 640 and desktop ≥ 1024.

Changed

1 entry
  • Footer: removed developer-facing links; added certification trust band and Cookie Policy link (src/components/triforce/site-footer.tsx, src/lib/i18n.ts)

    A Sotheby's-level reviewer or GCC buyer's counsel doing due diligence would spot "Docs" and "Design system" in the services footer column and immediately read "developer side project, not a mature operator." Fix removes those internal endpoints from the buyer-facing footer entirely. Adds a certification trust band (ARGUS Platinum · IS-BAO Stage 3 · EURAMI Accredited) between the link grid and the legal disclaimer — the three credentials every institutional buyer's safety team looks for, surfaced on every page. Also adds the Cookie Policy link (just added in the previous PR) to the Account & Legal column, completing the legal link inventory. Both new i18n keys (footer.cookies, footer.certifications) are wired into both en and ar dictionaries; certification abbreviations carry data-keep-ltr so they render correctly in RTL Arabic context.

Added

2 entries
  • Arabic /about and /services pages (src/app/ar/about/page.tsx, src/app/ar/services/page.tsx)

    A GCC buyer doing due diligence on a $90M aircraft purchase hits "About" and "Services" early — both were 307-redirecting to English, which signals an incomplete product to the exact audience we're pitching. Fix ships complete Arabic-language versions of both pages with proper MSA copy written for warm, precision and appropriate register (not a word-for-word translation). `/ar/about` mirrors the full English About page: stats band (6 قارات / 42 دق median wheels-up / +11,400 patient transports / 99.97% dispatch rate), the founding creed (الشرف · الكرامة · الاحترام · الثقة), mission statement, four operating principles, seven-chapter timeline from 2003 to 2026, global-footprint base grid (seven cities), leadership team, six certifications panel, and a dispatch CTA. `/ar/services` covers all six service lines (فحص الطائرات, الصيانة وتجديد المقصورة, لوجستيات قطع الغيار, السمسرة, تحسين الأسطول, إدارة وتدريب الطاقم) plus the concierge desk, the 2003→2014→Today heritage timeline, the independence pledge, and a CTA. Both pages: (1) registered in AR_BUILT_ROUTES in src/middleware.ts so they no longer fall back; (2) set Metadata.alternates.languages via buildHreflangAlternates; (3) marked localized: true in src/app/sitemap.ts so Google discovers both /about + /ar/about and /services + /ar/services with correct hreflang alternates. RTL-safe layout throughout: start-*/end-* logical utilities, data-keep-ltr on IATA codes, year numbers, ICAO acronyms, and the phone number. Coverage matrix in docs/I18N.md updated.

  • GDPR/PECR compliance — Cookie Policy page + consent banner (src/app/cookies/page.tsx, src/app/ar/cookies/page.tsx, src/components/triforce/cookie-consent.tsx)

    The site had no cookie disclosure surface at all — a first-look legal red flag for any buyer's counsel reviewing data-protection compliance. Fix ships three pieces: (1) /cookies — a full English Cookie Policy with a per-cookie table (triforce_session), local-storage inventory, third-party vendor disclosure (Mapbox geocoding is the only vendor that sees a browser request), and per-browser instructions for managing cookies. (2) /ar/cookies — full Arabic translation in MSA, warm and precise tone appropriate for GCC private-aviation buyers; registered in AR_BUILT_ROUTES in middleware and in sitemap.ts with localized: true so both English and Arabic URLs appear in the sitemap with correct hreflang alternates. (3) <CookieConsent> — a lightweight Liquid Glass banner (iOS 26 liquid-glass class, backdrop blur, animated slide-up) wired into the root layout. Appears 1.8 s after first visit, persists the dismissal in localStorage so it never shows again, is locale-aware (links to /cookies or /ar/cookies depending on the active locale), and is suppressed on the immersive simulator view. The i18n dictionary gains three new keys (cookie.banner.message, cookie.banner.policy, cookie.banner.accept) in both en and ar to satisfy TypeScript's compile-time completeness check. Legal note: the site's single cookie (triforce_session) is strictly necessary under ePrivacy / PECR and exempt from prior-consent requirements; the banner satisfies the GDPR Arts. 13/14 transparency obligation rather than functioning as a consent gate. Should be reviewed by counsel before any marketing analytics are added.

Fixed

1 entry
  • A11Y — mobile nav drawer focus management and inert isolation (src/components/triforce/site-header.tsx, src/lib/i18n.ts)

    The hamburger menu used CSS grid-rows: 0fr to collapse visually, but the links inside it remained reachable via Tab when closed and focusable by screen readers at all times — a WCAG 2.4.3 / axe violation on every page. Fix adds three behaviours: (1) the collapsed drawer now carries inert={true}, which removes all its interactive elements from the tab order *and* the accessibility tree in one attribute; (2) when the drawer opens the page content (div.splash-content) is marked inert so keyboard users cannot Tab past the open menu into the content behind it; (3) focus is moved to the first link in the drawer 50 ms after open (giving the CSS grid animation time to start) and returned to the hamburger trigger when the menu closes via Escape or the × button — click-through navigation to a page naturally hands focus to the new route. The trigger button gains aria-haspopup="true" and the drawer gets role="navigation" with a localised aria-label (English: "Site navigation", Arabic: "التنقل في الموقع") added to both locale dictionaries in i18n.ts. No visual change; no reduced-motion regression; all three breakpoints unaffected (the menu is lg:hidden).

Added

1 entry
  • Toast notification system (src/components/ui/toast.tsx)

    New global glass-styled notification primitive that fills the previous zero-feedback gap on a $90M sales site. Implements a module-level singleton store (no context provider, no prop-drilling) so any client component can call toast.* with a single import. Public API: toast.success(msg), toast.error(msg), toast.info(msg), toast.warning(msg), toast(msg, opts), toast.dismiss(id). Each notification renders as an iOS 26 Liquid Glass card (liquid-glass, backdrop-blur-xl, glass rim inset shadows) with a Phosphor icon (variant- coloured: emerald for success, --color-aa red for error, amber for warning, muted for info), optional description line, and an accessible close button. A countdown progress bar (@keyframes toast-shrink) gives users a visual sense of auto-dismiss timing; it is hidden entirely via motion-reduce:hidden for vestibular-disorder users. Enter animation (translate-y-3 → 0 + opacity) and exit animation share the same 300ms transition-all guarded by motion-reduce:transition-none. role="alert" / aria-live="assertive" for errors; role="status" / aria-live="polite" for everything else; aria-atomic so screen readers announce the full message. Dismiss is idempotent (guards against double-call from the transitionend handler and the 350 ms fallback timeout for reduced-motion environments). RTL: container uses end-* logical Tailwind utilities so the stack correctly appears at bottom-left in Arabic and bottom-right in English. Z-index 9200 sits above all page content and the ChatBot widget (z-[55]) but below the skip-to-content link (z-[9999]). Bottom offset uses env(safe-area-inset-bottom) + a fixed clearance to stack above the mobile bottom-nav and the ChatBot trigger on all three breakpoints. <Toaster /> added to the root layout body (src/app/layout.tsx) — a single line, no wrapper needed. @keyframes toast-shrink added to globals.css. Wired to two existing components for immediate visible impact: share-button.tsx now calls toast.success("Link copied to clipboard") on clipboard write (replacing the invisible aria-live span that provided zero visible feedback); favorite-button.tsx calls toast.success on save and toast.info on unsave, both including the aircraft label so the confirmation is specific ("Gulfstream G650ER saved"). Zero breaking changes to any callers. (src/components/ui/toast.tsx, src/app/globals.css, src/app/layout.tsx, src/components/triforce/share-button.tsx, src/components/triforce/favorite-button.tsx).

Changed

1 entry
  • Button — premium border radius, smooth transition, and richer hover states

    The Button component (src/components/ui/button.tsx) previously used rounded-md (6 px) on every CTA sitewide — a jarring mismatch against the rounded-2xl / rounded-3xl glass cards and rounded-full navbar pill that surround them on every page. Changed to rounded-2xl (16 px) to bring buttons into alignment with the design system's soft-luxury language. Transition updated from transition-colors (no duration) to transition-all duration-300 — all animatable properties now ease together on hover rather than snapping. Primary variant gains motion-safe:hover:-translate-y-px (1 px lift on hover, spring-out easing, gated behind prefers-reduced-motion so vestibular-disorder users see none of it) — the physical lift signals "interactive" without distracting copy. Outline variant gains hover:bg-accent-soft so hover produces a tinted glass fill rather than only a border/text colour change; reads as a genuine second state on a luxury surface. Focus-visible ring now carries focus-visible:ring-accent on the base class so keyboard focus is accent-coloured on all three variants (previously only primary was wired; outline and ghost showed browser-default blue). motion-reduce:transition-none added to base to completely remove motion for reduced-motion users. Zero changes to sizes, block behaviour, or the component's polymorphic render logic; entirely backward-compatible.

Added

2 entries
  • Safety & Certifications page (/safety + /ar/safety)

    New dedicated safety and compliance page covering Triforce's six active aviation certifications — ARGUS Platinum (since 2011), Wyvern WINGMAN (since 2014), IS-BAO Stage 3 (since 2017), FAA Part 135 (since 2003), EASA AOC (since 2006), and CAMTS Accredited (since 2009). Each certification is presented in a liquid-glass card with the issuing body, a year-of-award chip, and a detailed description of why that body matters to a buyer's auditor. A four-stat glass panel surfaces the safety record (23+ years incident-free, 6 active certs, 187 countries permitted, 100% safety record). A safety-culture section quotes the Chief Flight Operations Officer directly. A due-diligence CTA at the foot explains that audit reports, MEL documentation, crew records, and training logs are available under NDA to qualified buyers. The Arabic counterpart (/ar/safety) ships in the same commit with full MSA copy, RTL logical utilities, and data-keep-ltr guards on all Latin cert names and issuer identifiers. Route registered in AR_BUILT_ROUTES (middleware) and STATIC_PATHS (sitemap, localized: true, priority 0.85). Footer link added to both locales. Sixteen new i18n keys added to the Dictionary type; TypeScript enforces completeness in both English and Arabic dictionaries. docs/I18N.md coverage matrix updated. (src/components/triforce/safety-certifications.tsx, src/app/safety/page.tsx, src/app/ar/safety/page.tsx, src/lib/i18n.ts, src/middleware.ts, src/app/sitemap.ts, src/components/triforce/site-footer.tsx, docs/I18N.md).

  • SEO — Organization + WebSite JSON-LD on every page

    Two site-wide schema.org blocks are now injected into <head> by the root layout (src/app/layout.tsx) and appear on every English and Arabic route. The Organization block (#organization) declares the brand entity: name, logo (SVG), OG hero image, full site description, two ContactPoint entries (customer-service + emergency, both 24/7, both English + Arabic), and a sameAs reference to @triforceaero. The WebSite block (#website) links back to #organization as publisher and declares inLanguage: ["en", "ar"]. Both use stable @id anchors so page-level schemas (BreadcrumbList, FAQPage, Article) can cross-reference the entity without duplicating it. Prior to this change, Google Rich Results Test, Lighthouse SEO, and Schema.org Validator all returned zero entity markup — a gap that flags immediately in buyer technical DD and suppresses the brand Knowledge Panel. docs/SEO.md updated.

Fixed

1 entry
  • A11Y — prefers-reduced-motion coverage for all inline animations

    Every animate-spin, animate-ping, and animate-pulse usage that lacked a reduced-motion guard now carries the appropriate Tailwind variant: loading spinners get motion-reduce:animate-none (icon stays visible, rotation stops); decorative ping rings and status-dot pulses get motion-safe:animate-ping/pulse (outer ring is suppressed entirely for reduced-motion users while the static base dot remains). Affects 13 sites across 10 files: sim-loading-veil.tsx, hud-overlay.tsx (STALL/OVERSPEED warnings), admin/page.tsx, missions/page.tsx, about/page.tsx, account/account-actions.tsx, sign-in/sign-in-form.tsx, air-ambulance-intake.tsx, trip-builder.tsx, request/charter/page.tsx, request/air-ambulance/page.tsx. Resolves WCAG 2.3.3 (Animation from Interactions) and WCAG 2.1.2 (Pause, Stop, Hide) for vestibular-disorder users. Zero visual change for users without the OS reduced-motion preference set.

Added

3 entries
  • Process Stepper — "How It Works" component

    New reusable ProcessStepper UI primitive (src/components/triforce/process-stepper.tsx). Renders a numbered, icon-led step flow in liquid-glass cards: 1-column on mobile, 2-column on tablet, 4-column on desktop. Animation uses the existing ios-reveal / ios-stagger scroll-driven system (relies on the page-level <RevealOnScroll /> observer — no extra JS bundle). RTL-correct throughout via Tailwind logical utilities. Wired into both /private-jets and /ar/private-jets as a "Charter in four steps" section positioned between the featured aircraft grid and the popular routes band. Arabic copy is MSA, written for Gulf private-aviation buyers. (src/components/triforce/process-stepper.tsx, src/app/private-jets/page.tsx, src/app/ar/private-jets/page.tsx).

  • Aircraft Comparison Table (/private-jets/compare + /ar/private-jets/compare)

    New side-by-side charter fleet comparison page. All five aircraft (Pilatus PC-12 NG, Embraer Phenom 300, Cessna Citation Longitude, Gulfstream G650ER, Bombardier Global 7500) are shown in a single horizontally scrollable table: hero thumbnail, type badge, range, top speed, max altitude, passenger capacity, cabin length, indicative charter rate, and "Ideal For" summary. Best-in-class cells are highlighted in accent gold with a labelled star badge (e.g. "Longest range", "Fastest", "Most seats"). Sticky label column keeps row context visible while scrolling on narrow viewports. Arabic counterpart ships in the same PR with full MSA copy, RTL sticky-column behaviour, and data-keep-ltr guards on Latin numeric strings. A "Compare all aircraft" link added to both /private-jets and /ar/private-jets. Route registered in AR_BUILT_ROUTES (middleware) and STATIC_PATHS (sitemap, localized: true). (src/components/triforce/aircraft-compare.tsx, src/app/private-jets/compare/page.tsx, src/app/ar/private-jets/compare/page.tsx, src/middleware.ts, src/app/sitemap.ts, src/app/private-jets/page.tsx, src/app/ar/private-jets/page.tsx).

  • Daily-agenda scheduler

    New GitHub Actions workflow (.github/workflows/daily-agenda.yml) runs every morning at 05:30 UTC (09:30 GST / 01:30 ET) and writes docs/agendas/YYYY-MM-DD.md to main. Each file snapshots open PRs (draft vs ready, author, head branch), commits shipped to main in the last 24h, open issues, claude/* branch sprawl, and the standing roadmap reminders from CLAUDE.md, with a blank "Today's focus" checklist for the operator to fill in. The generator is a portable bash script (scripts/generate-daily-agenda.sh) that uses gh + jq, so it runs locally too (./scripts/generate-daily-agenda.sh [YYYY-MM-DD]). The workflow also exposes workflow_dispatch with an optional date input for backfilling. Bootstrapped today's agenda by hand (docs/agendas/2026-05-15.md); subsequent days are auto-generated. Index lives at docs/agendas/README.md.

Changed

2 entries
  • CapabilityBand + FeatureRow: liquid glass card treatment

    The four key capability pillars ("24/7", "Rapid Response", "Global", "Safety") on the English and Arabic home pages were upgraded from a bare bordered grid to individual liquid glass micro-cards, bringing them into visual parity with the StatReveal, testimonials, and "Beyond the flight" sections. Each card now has a rounded-2xl glass surface with inset rim highlights, a subtle spinning liquid-gradient accent orb at top-right, and a glass-chip circle icon container. The flat border-y divider is replaced by the cards' inherent elevation. FeatureRow icon containers are similarly upgraded from basic rounded-md border squares to liquid-glass-chip rounded-xl pills. (src/components/triforce/feature-row.tsx)

  • StatReveal: copy polish + premium glass-card treatment

    The section eyebrow changed from the informal "The receipts" (American slang, off-brand for GCC buyers) to "Track record". Two detail strings were tightened: "ICU transports flown 2025" → "Air medical missions flown" (timeless, grammatically complete) and "Median pickup 11s" → "11 s median pickup" (consistent formatting). The stat grid is now wrapped in a dark glass card with a liquid-gradient orb — matching the treatment used on the "Beyond the Flight" section — so the four credential numbers feel anchored and intentional rather than floating loose on the page (src/components/triforce/stat-reveal.tsx).

Added

1 entry
  • Private Jet Ownership Cost Calculator (/financing + /ar/financing)

    Interactive total-cost-of-ownership tool for prospective aircraft buyers. Covers five fleet aircraft (PC-12 NG through Global 7500), three ownership structures (cash, financed, managed charter), live sliders for down payment, loan term, interest rate, annual hours, Jet-A fuel price, maintenance tier, and hangar. Results panel shows annual cost breakdown, cost per private hour, and five-year projected spend — all live-updating. Arabic counterpart ships in the same PR with full MSA translation. Linked from the site footer under Services. (src/components/triforce/ownership-calculator.tsx, src/app/financing/page.tsx, src/app/ar/financing/page.tsx, src/lib/i18n.ts, src/middleware.ts, src/app/sitemap.ts, src/components/triforce/site-footer.tsx; docs: docs/FINANCING.md).

Changed

7 entries
  • Simulator: camera starts farther back and top controls use More

    The chase camera now seeds at its follow point instead of easing out from inside the aircraft and starts with a wider runway view. The crowded top toolbar was reduced to mode / camera / pause / reset / More; time of day, failures, fidelity, leaderboard, controls, sound, fullscreen, and VR now live in a compact settings menu with English + Arabic labels (src/components/simulator/{core/camera-rig,flight-simulator}.tsx, src/lib/i18n.ts; docs: docs/SIMULATOR.md).

  • Simulator: aircraft picker is now a toggled bottom sheet

    The selected aircraft button stays visible at the bottom of the sim; tapping it slides the jet picker up, and choosing an aircraft slides the picker back down so the flight view is not permanently covered. Expanded aircraft cards now keep the generated thumbnail in a fixed 1:1 square beside the details instead of a tall image-first block. Touch / pointer controls now occupy a fixed non-overlapping control zone while the picker is collapsed and hide while the picker is open; the phone HUD also drops the route-detail panel so it cannot collide with the attitude indicator (src/components/simulator/{flight-simulator.tsx,hud/aircraft-picker.tsx,hud/touch-controls.tsx,hud/hud-overlay.tsx}; docs: docs/SIMULATOR.md).

  • Simulator: roll/pitch joystick is now available on desktop and tablet

    The virtual stick was previously tied to touch-only phone controls; it now renders as a dedicated pointer joystick on md+ viewports while phones keep the full throttle / rudder / gear control cluster. Phone controls were also raised so the stick and throttle no longer cover the active aircraft card (src/components/simulator/hud/touch-controls.tsx, src/components/simulator/flight-simulator.tsx; docs: docs/SIMULATOR.md).

  • Canonical public domain updated to triforce.flights

    Replaced the previous .aero origin across site metadata, sitemap copy, contact links, email defaults, admin sample data, and documentation so generated canonical URLs and visible product copy now point at the correct domain.

  • Simulator: free-flight A→B nav + moving map + GLB load reliability

    (src/components/simulator/core/{free-route,sim-core}.ts, models/{cockpit,aircraft-glb}.ts, core/world.ts). Default route KTEB → OMDB with real great-circle distance; HUD course chip shows bearing / remaining sim distance; cockpit MFD draws a heading-up magenta line to the arrival code; a tall magenta waypoint marks the enroute point in the 3D world for chase/orbit cameras. Aircraft GLB loads no longer depend on a HEAD pre-check (some hosts returned 405 / wrong Content-Type, which skipped the real model). Ocean uses a smoother normal field, lower UV repeat, slower scroll, subdivided plane, slightly lower sea plane, and no polygon-offset hack; terrain base mesh + near patch got more segments and texture anisotropy; renderer enables logarithmic depth buffer to reduce far-scene Z shimmer.

  • Simulator: aircraft picker layout

    Fleet strip cards now use a fixed 1:1 generated thumbnail (52 px) beside the name, category, and speed / range lines instead of a tall image-first layout. The generator now renders a tighter square source image so aircraft fill the tile instead of appearing tiny, while the strip stays compact on desktop, tablet, and phone viewports.

  • Simulator: texture colorSpace and anisotropy standardized

    All GLB aircraft models and procedural textures now apply proper Three.js texture configuration: color maps (albedo, emissive) use SRGBColorSpace for correct sRGB interpretation, while data maps (roughness, metalness, normal, AO, env) use NoColorSpace for linear data. All textures apply anisotropy = 4 for improved quality at oblique angles. Verified across all 7 aircraft models (Gulfstream G650ER, Bombardier Global 7500, Cessna Citation Longitude, Pilatus PC-12 NG, Embraer Phenom 300, Airbus H145) and VFX generators (src/components/simulator/models/aircraft-glb.ts, src/components/simulator/core/effects.ts).

Fixed

8 entries
  • Simulator: Phenom 300 no longer flies with the cabin door open

    The Embraer Phenom 300 GLB has been post-processed so the static ground stairway is removed from the scene and the cabin entry is covered by a closed door/livery patch, keeping the fleet viewer and in-flight simulator from showing boarding hardware mid-air (public/models/aircraft/embraer-phenom-300/model.glb; docs: docs/SIMULATOR.md, docs/CREDITS.md).

  • Fleet 3D viewer: AR QR codes now open a mobile AR handoff instead of the current page

    The desktop "View in AR" dialog encodes /api/ar/aircraft/<slug> with the current fleet detail page only as a fallback. Android phones are sent to a Scene Viewer ar_only intent from the GLB, iOS redirects to Quick Look when the endpoint confirms the USDZ is available, and fallback page loads carry ar=1 so the 3D viewer activates immediately. The QR dialog is portaled to document.body so it stays viewport-centered through the splash handoff. QR dialog copy is localized for English and Arabic (src/components/aircraft/aircraft-hero.tsx, src/app/api/ar/aircraft/[slug]/route.ts; docs: docs/MODELS_AR.md).

  • Lint: generated Draco assets are ignored

    ESLint no longer walks public/draco/** or local .claude/** worktrees, so simulator checks focus on app code instead of minified decoder/vendor blobs (eslint.config.mjs).

  • Fleet cards: compact side-by-side thumbnails

    Shared aircraft cards now use a fixed 1:1 thumbnail beside the aircraft details instead of a large vertical 16:9 image block, preventing desktop fleet grids from feeling oversized or letting long names/specs crowd each other.

  • Mobile chat launcher: bottom-nav clearance

    The collapsed Ask Tri control is now icon-only on phones and the open panel clears the fixed bottom nav, so it no longer blankets fleet-card details in narrow viewports.

  • CSP: Three.js blob texture loading

    connect-src now permits blob: so GLB texture object URLs used by the splash/simulator loaders are not blocked by the site Content Security Policy.

  • Simulator: iOS / home-screen safe areas

    Exit chip, launch overlay, mode/time-of-day rails, and leaderboard panel now offset with env(safe-area-inset-*) so content clears the status bar, Dynamic Island, and home indicator when the route runs full-viewport (viewport-fit=cover).

  • Simulator: H145 / GLB main rotor spin axis

    The sim always drove rotation.y on the loaded main-rotor node; many Sketchfab rigs use a different local axis for the mast after our body-frame fix-ups, which read as a “sideways” disc. After the GLB is parented to the airframe group, the sim now picks the local Euler axis best aligned with body +Y in world space (with an optional per-slug rotorMainSpinAxis override in GLB_FIXUP).

Added

1 entry
  • Simulator → World Tour: fly between two real airports over real terrain

    A new lazy-loaded route (/simulator/world-tour, /ar/simulator/world-tour) that flies a great-circle leg between two real airports — pick a departure and an arrival (curated GCC / Alps / Rockies list, plus quick-start pairs), watch the auto-flown cinematic (takeoff → climb → cruise → descent → touchdown) over genuine terrain, and drag to look around. Elevation is streamed from the public AWS "Terrain Tiles" open dataset (Terrarium PNGs) through a new same-origin proxy GET /api/terrain/[z]/[x]/[y]no API key, no satellite imagery (ground colour is elevation/slope driven). New code: src/app/api/terrain/[z]/[x]/[y]/route.ts, src/components/simulator/world-tour/ (geo.ts, airports.ts, world-tour-scene.ts, world-tour.tsx, world-tour-client.tsx), src/app/simulator/world-tour/ + src/app/ar/simulator/world-tour/. Linked from the simulator launch screen; bilingual; respects prefers-reduced-motion (slow static overview) and low-power devices. Docs: docs/WORLD_TOUR.md.

Fixed

6 entries
  • Simulator: retractable landing gear now folds into the belly with correct pivots

    Previously the entire gear assembly was rotated ~90° about the fuselage centre, so the legs swung out in a wide arc instead of stowing. Each leg now pivots about its own attach point — the nose leg swings forward and up, the two main legs fold inboard toward the centreline — which reads as the gear tucking under the belly. The legs are still procedural geometry (not a separate model), built in parts.ts / aircraft-model.ts and animated in sim-core.ts.

  • Simulator (phone mode): the top header now clears the iOS status bar / notch

    In the wrapped iOS build the safe-area inset eats the top of the viewport, so the Exit chip and the mode/camera toolbar were jammed against (or under) the system clock. Both now sit at top-[max(0.75rem,env(safe-area-inset-top))] — unchanged on the web, pushed down by the inset in the app — and the desktop time-of-day rail follows the same offset (src/components/simulator/flight-simulator.tsx).

  • Simulator (phone mode): top toolbar no longer overlaps the Exit chip or gets veiled

    The mobile mode/camera toolbar was full-bleed (inset-x-2), so it ran underneath the Exit chip, and it sat at z-20 — the same layer as some other top chrome — so it could be painted over. It now starts clear of the Exit chip (start-[5.25rem] … end-2) and sits at z-30, with the launch / leaderboard / unsupported surfaces lifted to match. The six-button time-of-day rail is now desktop-only (md:flex) — on a phone it just stacked under the toolbar and crowded the HUD; the simulator opens in golden-hour and the rail returns at md (src/components/simulator/flight-simulator.tsx).

  • Simulator: Embraer Phenom 300 GLB now faces forward

    Our best jet model so far is authored nose-along the X axis (not the Z axis the Sketchfab jets use) but pointing aft, so the shared rotY: π/2 fix-up laid it across the runway; the fix-up is now rotY: π. Its wingspan is within 15% of its length, so the auto "longest horizontal axis = forward" safety net stays dormant and won't re-rotate it (src/components/simulator/models/aircraft-glb.ts).

  • Simulator: Pilatus PC-12 NG GLB no longer loads belly-up and backwards

    Helijah's PC-12 model already bakes the Z-up → Y-up conversion into its Sketchfab_model root-node matrix, so the per-slug rotX: -π/2 fix-up was double-rotating it onto its back; the modelled propeller also sits at the body's -X end, so the nose pointed aft. The fix-up is now rotY: π (no rotX), which leaves the model upright with the nose at +X (src/components/simulator/models/aircraft-glb.ts).

  • Simulator (phone mode): virtual joystick knob now sits dead-centre

    The on-screen stick's knob combined Tailwind's -translate-x-1/2 -translate-y-1/2 utilities with a JS-managed transform. Under Tailwind v4 those utilities emit the translate CSS property, which is *additive* to transform, so on release the knob was offset by roughly its own size instead of resting in the centre of the base. Centring is now owned solely by the inline/JS transform (src/components/simulator/hud/touch-controls.tsx).

Changed

1 entry
  • Simulator pages: disable text/image selection

    The /simulator and /ar/simulator <main> wrappers now carry select-none plus -webkit-user-drag:none on images, so dragging the canvas or long-pressing the HUD no longer selects/ghost-drags the overlay text and graphics (src/app/simulator/page.tsx, src/app/ar/simulator/page.tsx).

Added

8 entries
  • Simulator: auto-generated 3D thumbnails on the aircraft picker

    Each pill in the bottom-of-screen jet picker now carries a little rendered preview of the aircraft's real GLB model — banked three-quarter "hero" pose on a per-aircraft "vibe" gradient tile (hand-picked colours: the G650ER's midnight-to-cyan speed, the Global 7500's long-haul twilight, the PC-12's Royal-Flying-Doctor outback ochre, the H145's emergency-services red, …), with the (big, bold) plane floated up out of the tile so the nose breaks the frame. A single shared 480×288 offscreen WebGL renderer paints each model once (src/components/simulator/models/aircraft-thumbnail.ts), reusing the sim's own tryLoadAircraftGLB normalisation so the thumbnail matches what you fly. Work is lazy (only pills near the viewport — and the selected one — generate), serialised (one model in flight at a time), cached in-memory and sessionStorage, and the extra WebGL context is torn down ~5 s after the queue goes idle. As a bonus it warms the browser HTTP cache, so picking that aircraft loads its model from cache. Falls back to the aircraft hero photo when WebGL/the model isn't available; honours prefers-reduced-motion (no fade-in). src/components/simulator/hud/aircraft-picker.tsx.

  • Simulator: engine fire / afterburner "ring of fire" at the jet nozzles

    (src/components/simulator/models/aircraft-model.ts, src/components/simulator/models/skin.ts, src/components/simulator/core/sim-core.ts). Each jet engine now carries a three-layer additive flame parked at the exhaust nozzle — a glowing core, a bright ring of fire annulus, and a 3D plume cone tapering aft — pulsed every frame from the engine spool with layered, per-engine de-phased flicker. The plume only really lights up under high thrust (takeoff / climb). The FX group is flagged keepWithGlb, so it stays visible after the real Sketchfab GLB takes over the silhouette. Two new cached canvas textures (getFireCoreTexture, getFireRingTexture) freed via disposeSharedSkins().

  • Simulator: pinch-to-zoom on touch devices

    (src/components/simulator/core/camera-rig.ts). Two-finger pinch now dollies the camera in/out — in *orbit* mode it scales the orbit radius (8–60 m, same range as the desktop scroll wheel), and in *chase* mode it scales the follow distance/height (0.5×–3×) so you can push the aircraft further away on a phone. The simulator stage is now touch-action: none so the gesture reaches the canvas instead of triggering browser page-zoom, and the scroll-wheel zoom now works in chase mode too (previously orbit-only).

  • Simulator: ground LOD + terminal + fuel farm extending the takeoff zone

    (src/components/simulator/core/world.ts). Builds on the airport added in #158 with the pieces that were still missing:

    • Near-field terrain LOD. A 4.2 km / 220-segment high-density patch centred on the airport sits 2 cm above the 18 km / 168-segment base mesh and shares its splat material. Same heightAt() sampling so the two meshes are geometrically congruent — the patch just provides tighter tessellation where the camera spends takeoff/landing time. Tucked under the runway strip (0.05) and blast pads (0.04) so it never z-fights the painted markings.
    • Tree scatter LOD. Splits the 2 200-tree population into two tiers: full trunk + 7-sided canopy within 2.2 km of the field, trunkless 4-sided cone beyond. Roughly halves per-tree triangle count on the bulk of the population without changing the cruise-altitude silhouette (fog absorbs the simpler far cones before the side-count reads).
    • Terminal building. 240 × 22 × 18 m slab on the far edge of the apron with a procedural window-facade material on the runway-facing side (random mix of warm-lit and cool-reflective cells with thin mullion bars), darker roofline trim, three retracted-jetway-stub piers and precast-concrete panel texture on the other faces.
    • Jet A-1 fuel farm. Three bulk tanks (cylinders r = 14 m / h = 18 m with hemispherical caps) painted white with weld seams, red safety hoop, and a "JET A-1" stencil — sitting inside a low concrete containment berm with a pump-house stub.
    • Wider campus flatten. heightAt() flatten extended asymmetrically to a 540 m half-width on the −Z campus side (previously 420 m everywhere) using a smoothstep, so the existing ramp + tower + hangars *and* the new terminal/fuel farm sit on level ground.
    • All textures are procedural canvas — no new image assets shipped.
  • Jet detail pages: "Fly This Jet in the Simulator" CTA

    (src/app/{private-jets,air-ambulance,ar/private-jets,ar/air-ambulance}/fleet/[slug]/page.tsx, src/app/{,ar/}simulator/page.tsx, src/components/simulator/{simulator-client,flight-simulator}.tsx, src/lib/i18n.ts). Every jet details page now shows a third CTA between *Request This Aircraft* and *Speak with Concierge / Call 24/7 Emergency* that deep-links into the simulator with the current airframe preselected (/simulator?aircraft=<slug>, or /ar/simulator?aircraft=<slug> on the Arabic routes). The simulator pages now read the aircraft search-param and pass it through to the client as initialSlug; FlightSimulator validates the slug against getAircraft(...) and falls back to the default G650ER if it's missing or unknown, so the CTA can't land the buyer on a broken sim. Arabic copy (aircraft.cta.flySimulator: "جرّب هذه الطائرة في المحاكي") ships in the same change.

  • Simulator: pause-mode paper-plane cursor

    (src/components/simulator/flight-simulator.tsx). When the simulator is paused the system cursor swaps to a small white-on-dark paper-plane glyph (32×32 SVG, hotspot at the nose) — a low-effort but on-theme touch so the pause overlay doesn't feel like a generic dimmed page. Mouse-fly's crosshair cursor is suppressed while paused so the two never fight.

  • Simulator: detailed airport / landing zone for takeoff perspective

    (src/components/simulator/core/world.ts). Replaced the placeholder runway (asphalt strip + dashed centreline + 6 piano-key stripes per end + 40 instanced edge lights + two grey hangar boxes) with a working aerodrome that reads from the cockpit during the takeoff roll:

    • FAA-style runway markings — continuous white side stripes, 8-stripe piano-key thresholds, "09" / "27" designators, paired aiming-point bars 300 m past each threshold, 3-2-1 touchdown-zone marker groups, FAA-spaced centreline dashes (~67 m cycle), sealcoat seams, weathering speckles, and rubber-deposit smudges in each TDZ. 4× larger canvas (512 × 4096) and 8× anisotropy so the markings stay legible from the aircraft datum out to the far threshold.
    • Edge / threshold / end lights. Edge lights are now spaced ~60 m along both sides (54 stations) with the last 600 m of each end going amber per FAA spec; dense 14-light green threshold bars and red end- of-runway bars cap each end.
    • PAPI — two 4-unit PAPI arrays, one per landing direction, on the correct side of the centreline relative to each approach.
    • Approach lighting — five cross-bars of white lights extending 300 m past each threshold, on small dark posts.
    • Parallel taxiway with yellow centreline + yellow dashed edges + two connector taxiways and runway holding-position bars where each connector meets the strip.
    • Blast pads — concrete plates with yellow chevrons ahead of each threshold.
    • Distance-remaining signs every ~305 m down each side of the strip.
    • Windsock on a banded red/white pole with an animated fabric drape that sways with the prevailing wind (same wind direction the clouds drift at, so the apron and sky tell one consistent story).
    • Control tower (shaft + 14-sided glass cab + brim + roof) with a rotating beacon — a recognisable landmark in the mid-distance.
    • Ramp — concrete apron with painted parking T-bars, two hangars (now with sliding-door fronts), a row of three parked light aircraft, and two ground-service vehicles. The animation hooks (windsock, beacon) are driven from World.update(dt, t) so they cost a handful of Object3D.rotation writes per frame.
  • Simulator: cockpit interior + ring-trial medals & leaderboard

    (src/components/simulator/**):

    • First-person cockpit interior is now wired. The procedural PBR flight deck scaffolded in #145 (models/cockpit.ts) — instrument panel, glareshield with accent pinstripe, three-screen Garmin-style avionics (PFD / MFD / EICAS) painted via CanvasTexture, side console + thrust quadrant + flap lever, sidestick / yoke / cyclic + collective per airframe type, windshield posts, top rail, ceiling overhead-panel strip and a back-side cabin shell — is now built inside SimCore, parented to the aircraft at the pilot eyepoint, and shown when the camera is in Cockpit mode. Avionics canvases repaint at ~6 Hz when actually visible (zero cost when nobody is looking): PFD attitude/airspeed/altitude tapes + HSI compass arc + course pointer; MFD moving map with range rings, course line and terrain profile; EICAS engine block with N1/N2 gauges + FUEL/OIL/HYD bars. The GLB-hide step skips the cockpit so it stays in place regardless of which aircraft asset loads, and is rebuilt on every aircraft swap.
    • Ring trial: medals + leaderboard. Each completed ring trial now earns Gold / Silver / Bronze medals against per-aircraft cutoffs scaled from that airframe's cruise speed (courseDistance / (cruise · {1.12, 0.90, 0.60}) · 1000) — so a G650 isn't held to a turboprop's bar. Top-5 times per aircraft are persisted to localStorage["tf.sim.ring.lb.<slug>"] and shown in a Leaderboard popover (toolbar Trophy button) and inline in the ring results modal alongside the medal cutoffs. New core/leaderboard.ts + hud/medal-badge.tsx. All new UI surfaces ship with Modern Standard Arabic copy (sim.medal.*, sim.leaderboard.*) in the same PR.

Changed

1 entry
  • Simulator: mobile UX overhaul

    (src/components/simulator/**). The phone layout was cramped — the mode/camera toolbar and the time-of-day rail both wrapped onto three lines and collided with the HUD, and the on-screen controls overlapped the PFD tapes. Now: both toolbars stay on a single line and scroll horizontally on small screens (scrollbar-none); the on-screen joystick is rebuilt — larger, with cross-hair guides, a glowing accent knob, an 8 % dead-zone and a spring-back release — and the throttle is a real vertical slider with a draggable handle and a live percentage read-out; rudder / gear / flap chips are bigger, regrouped and clear of the aircraft-picker strip, and the whole control cluster respects env(safe-area-inset-bottom). On phones the HUD now hides the FMA strip, fuel/gross-weight chips and α·g chips, shrinks the attitude indicator and course panel, and re-anchors the airspeed/altitude tapes near the top so nothing overlaps the controls. The autopilot panel is desktop-only on phones (it was ~60 % of the viewport width). New sim.controls.throttleShort / sim.touch.stick strings ship with Arabic copy.

Fixed

4 entries
  • Top progressive-blur band no longer veils on-page chrome

    (src/app/globals.css). .progressive-blur-top was z-index: 30 and is rendered after {children} in the DOM, so it painted *over* page chrome that also sits at z-30 — most visibly the aircraft pages' sticky 2D/3D toggle pill, which looked blurred/dimmed. Dropped the band to z-index: 20: still above page content, now cleanly below the navbar (z-40) and the sticky toggle (z-30).

  • Simulator: jet GLBs now sit on the runway centreline

    (public/models/aircraft/**). Four of the five shipped jet GLBs (G650ER, Global 7500, Citation Longitude, Phenom 300) had their geometry offset from the file origin — for the Phenom 300 by ≈ 40 m, ≈ 6× the body length. The runtime loader centres the joint bbox on the wrapper origin before applying the per-slug rotation, so an off-axis centroid in the GLB produced a longitudinal / lateral (R−I)·c_local offset that left the model visibly sliding off the centreline in chase, orbit, and external camera views. Re-centred each affected GLB in Blender so its combined mesh AABB sits at the file origin — the rotation is now neutral and every jet rides the runway centreline. Pilatus PC-12 was already centred (< 0.5 % offset) and was left untouched.

  • Simulator: H145 helicopter GLB now loads correctly

    (src/components/simulator/models/aircraft-glb.ts). GRIP420's EC-135 mesh bakes the Z-up → Y-up conversion into a root-joint quaternion (≈ 180° about the YZ diagonal); the previous per-slug fix-up applied a *second* rotX: -π/2, rotY: π on top of that, which tipped the model onto its side and hid the rotor — so the loader denied it and fell back to the procedural rotorcraft. The override now applies a single rotY: π/2 to bring the already-Y-up model's nose to +X. The per-helicopter blanket denial and the airbus-h145 entry in the GLB deny-list have both been removed. A new safety net in the loader detects a sideways orientation (size.z > size.x · 1.15) and rotates Y by 90° automatically, so a future GLB without a hand-tuned override still self-corrects to nose-forward.

  • Simulator: Pilatus PC-12 NG no longer spawns rolled 90°

    (src/components/simulator/models/aircraft-glb.ts). Helijah's Sketchfab PC-12 is authored in a CAD frame (X-forward, Y-span, Z-up — confirmed against the model's bbox: 14.4 m × 16.2 m × 4.2 m matches the real aircraft's length × wingspan × height). The previous rotY: π/2 fix-up was rotating around the nose-axis, which read as a 90° roll. Replaced with rotX: -π/2 so the model converts Z-up → Y-up cleanly while the nose stays along +X.

Added

6 entries
  • Simulator: WebXR (VR) + spatial audio

    (src/components/simulator/**):

    • Enter VR from the toolbar. SimCore now probes navigator.xr.isSessionSupported("immersive-vr") on startup and bubbles an xrSupported event up to React; when true, a VirtualReality toolbar chip appears next to the fullscreen button (EN: "Enter VR" / AR: "تفعيل الواقع الافتراضي"). One click opens an immersive-VR session via renderer.xr.setSession(...) with optional local-floor, bounded-floor, hand-tracking and layers features — works on any WebXR-capable headset (Quest Browser, Vision Pro Safari, Pico, WMR, Index/Vive desktop Chrome/Edge). The render loop swapped from requestAnimationFrame to renderer.setAnimationLoop, so frames are driven by the headset's XR animation frame while presenting and fall back to rAF on the flat canvas.
    • Camera dolly. The flat-mode PerspectiveCamera now lives under a THREE.Group ("dolly") which is parked at the cockpit eyepoint while a session is active. WebXR layers head pose on top of the dolly, so the pilot's head sits where the eyepoint would naturally fall in cockpit view — no fighting three.js for the per-eye matrices.
    • Spatial audio (HRTF). SimAudio routes the engine bus through a PannerNode (HRTF, inverse rolloff, ref-dist 6 m) placed at the aircraft position, and updates the AudioListener position + forward/up every frame from the camera's world matrix. Engine noise now pans with head movement (great on headphones, essential in VR); wind/tyre rumble and GPWS/cabin voice stay non-spatial so the pilot always hears callouts clearly.
  • Simulator: procedural HDR sky + time-of-day switcher

    (src/components/simulator/core/sky.ts, world.ts, sim-core.ts, flight-simulator.tsx):

    • Six lighting presets — Dawn, Morning, Noon, Golden, Dusk, Night. Each preset retunes the sky-gradient shader, sun position + color + intensity, hemisphere bounce, fog tint/range, ocean base color, ACES tone-mapping exposure, environment IBL intensity, and (night only) a cheap procedural star field. Selection is one click in a new centred "Time of day" chip rail under the top control bar; the choice persists in localStorage (triforce.sim.tod). Default = Golden for the launch screen vibe.
    • **The sky *is* the IBL.** Instead of shipping an .hdr asset, the same sky shader is rendered into a small dome inside a throw-away "env scene" and baked through PMREMGenerator.fromScene() on every TOD change — so reflective materials (G650 fuselage, ocean specular, metal nacelles, gate rings) pick up the actual sky color of the current preset. Lightweight: no extra network bytes, ~6 face renders per swap, and the bake reuses the already-loaded PMREM generator.
    • The visible sky shader is now shared (core/sky.ts) so the dome and the env-bake dome can never drift out of sync. EN + AR strings under sim.tod.* (Dawn / Morning / Noon / Golden / Dusk / Night, Arabic: الفجر / الصباح / الظهيرة / الساعة الذهبية / الغسق / الليل).
  • Simulator pro-grade upgrade — type-rated avionics, autopilot, failures, FDR

    (src/components/simulator/**, src/lib/i18n.ts). The simulator is now credible in front of a working pilot: every reading on the PFD comes from a published per-airframe number, the AP behaves like a real MCP, and the honest disclosure card documents exactly what is and isn't modelled.

    • Per-aircraft published performance database (src/components/simulator/core/performance.ts). MTOW / BOW / fuel, V1 / Vr / V2 / Vref / Vs / Vs0 / Vmo / Mmo / Vy / Vfe / Vle, service ceiling, climb rate, balanced-field length, landing distance, twin / single — all from each manufacturer's public AFM or FAA / EASA TCDS, cited per airframe in the Fidelity card. Vr / V2 / Vref scale to current gross weight (∝ √(W/Wref)) and drive live V-speed bugs on the PFD airspeed tape.
    • Coefficient-based flight-model upgrade (src/components/simulator/core/flight-model.ts). Adds ground effect on the fixed-wing branch (lift × 1.12, induced drag × 0.75 inside one wing-span of the surface — Wieselsberger curve), a sharper transonic drag rise beyond M ≈ 0.92, asymmetric thrust moment from an engine-out failure (yaw proportional to half-span × thrust delta), and a per-step FlightEnvironment so external systems can scale thrust / authority without touching the model.
    • Autopilot core (src/components/simulator/core/autopilot.ts) — a Garmin G5000-style mode state machine. Lateral: ROL / HDG / NAV. Vertical: PIT / ALT / VS / FLC. Speed (autothrottle): IAS / MACH. Bank limit, pilot-override drop-out (any axis disengages when the human stick exceeds threshold), and a 4-column FMA strip annunciated on the PFD top (SPD · LAT · VRT · ARMED). Wired into sim-core between the input system and the flight model, so AP commands and pilot inputs blend live.
    • MCP panel (src/components/simulator/hud/autopilot-panel.tsx) — a real Mode Control Panel in the top-right corner: master ON/OFF, mode chips, and HDG ±1° / ALT ±100 ft / VS ±100 fpm / IAS ±1 kt / MACH ±0.01 bug nudgers.
    • Failures & emergencies system (src/components/simulator/core/failures.ts, src/components/simulator/hud/failures-panel.tsx). Five toggleable malfunctions — left engine flameout, right engine flameout, hydraulic loss, cabin decompression, PFD failure — each with English + Arabic procedure copy. When armed, the failure annunciator strip lights up red just above the picker, the flight model reacts (asymmetric yaw on engine-out, ~60% authority loss on hydraulic), and the AP cannot paper over it.
    • Flight data recorder ("black box") (src/components/simulator/core/recorder.ts). 5 Hz ring buffer of 15 parameters (position, altitude, IAS, Mach, VS, heading, pitch, bank, AoA, g, throttle, flaps, gear) up to 30 min. Start / stop chip in the Failures drawer, CSV export for debrief.
    • Fuel burn + dynamic gross weight. Engines burn fuel from each airframe's published cruise consumption (kg/h × spool), gross weight drops continuously, which in turn shifts the live V-speed bugs. New FUEL and GW chips on the upper-left PFD.
    • Reference airport database (src/components/simulator/core/airports.ts). KTEB, KVNY, EGLF, LSGG, OMDB, VHHH, RJTT — every airport private-jet customers actually fly between — with elevation, lat/lon, runway designators, lengths, headings, surfaces and ILS frequencies. Not yet hooked to the procedural world (still procedural geometry); listed in the Fidelity card and ready for the approach / moving-map work.
    • Fidelity & data-sources card (src/components/simulator/hud/fidelity-card.tsx). The most important card on the sim from a due-diligence perspective. Plain-text "what's modelled / what isn't", per-airframe performance table with citations, reference-airport list. English + Arabic.
    • PFD redesign (src/components/simulator/hud/hud-overlay.tsx). New layout: FMA strip at top, attitude indicator below, heading strip, ASI tape with V-bug column, ALT tape with VS and RA, FUEL/GW chips, failure-annunciator banner.
    • Full Arabic translations for every new string — MCP labels, failure procedures, recorder controls, fidelity disclosure, audit table headers — all in MSA suitable for a GCC chief-pilot reviewer.
  • Simulator: wheels feel the ground

    (src/components/simulator/**):

    • GLBs visibly sit on the runway, never in it. Per-slug groundLift bumped (G650/Global 0.35→0.65 m, Longitude 0.30→0.55 m, Phenom/PC-12 0.25→0.50 m, H145 0.25→0.45 m; default 0.30→0.50 m) so the model's lowest point clears the strip with a comfortable margin. A render-time floor clamp in sim-core.ts re-asserts bodyY ≥ heightAt(x,z) + gearLength every frame so visual interpolation between physics steps can never sink the aircraft under the terrain.
    • Wheel suspension. New spring-damped vertical offset (wheelOffset, k=90, c=14, clamped to roughly ±0.1 m / −0.18 m) applied on top of the rigid-body height: hard touchdowns deliver a compression impulse proportional to sink rate; rolling on the runway adds a ~1 cm phase-driven bob; off-strip terrain adds a coarser ~6 cm wobble. Wheels now "feel" pavement and bumps instead of sliding glued to a single Y plane.
  • Simulator: Esc pause + credits, ocean & ground fixes

    (src/components/simulator/**):

    • Pause is now bindable to <kbd>Esc</kbd> (in addition to the toolbar button), and pausing shows a "Paused" overlay with a Credits panel — flight model / procedural world / glass-cockpit avionics credited to Elijah Royaie, plus the Three.js / WebGL / WebAudio / Web Speech tech stack and a third-party-asset note. The same credits appear on the launch screen and in the Controls overlay. New sim.paused / sim.controls.pause / sim.credits.* strings (EN + AR).
    • Sea fixed. The seabed was clamped to y = 0 — the same plane the water sat just *below* — so the ocean read as a glitchy z-fighting sand mess. The seabed now ramps down to ≈ −45 m offshore, the water plane sits at y ≈ −1 m, and the ocean material is opaque with a small camera-ward polygon offset. No more moiré.
    • Aircraft GLBs no longer sit buried in the runway. The ground-offset heuristic put the model's lowest point ≈ 0.13·length below the body origin (≈ 4 m on a G650 — well past the gear); it now anchors to the flight model's per-type gearLength plus a small lift so each aircraft rests *on* the surface.
    • Scaffolding added (not yet wired) for the next pass: a procedural PBR cockpit interior + Garmin-style glass-panel painter (models/cockpit.ts), a pooled particle/FX layer with engine heat-haze, contrails, crash fireball/debris/smoke and camera shake (core/effects.ts), a bloom + AO + grade post-processing pipeline that auto-disables on low-power devices (core/post.ts), and EGPWS aural callouts — "Pull up", "Sink rate", "Too low — gear", "Bank angle", radio-altimeter counts, "Minimums" — plus a crash impact sound in core/audio.ts.
  • Accessibility Statement

    (/accessibility + /ar/accessibility). Published a WCAG 2.1 Level AA accessibility statement covering conformance status, the specific measures in place (semantic HTML5, keyboard operability, prefers-reduced-motion compliance per WCAG 2.3.3, ARIA, bilingual RTL layout, colour contrast, error identification, zoom safety), known limitations with remediation targets (WebGL simulator, route-preview maps, live mission board), a 2-business-day feedback commitment, and escalation paths for UK (EHRC), EU, US (DOJ), and GCC jurisdictions.

    The Arabic page (/ar/accessibility) is a full translation in Modern Standard Arabic — not a machine-transliteration — consistent with GCC buyer expectations. The route is registered in AR_BUILT_ROUTES (middleware), added to the sitemap with localized: true and hreflang alternates, and linked from the footer "Account & Legal" column on every page. This resolves the gap a buyer's legal or tech DD reviewer would flag when checking WCAG compliance documentation.

Changed

2 entries
  • /simulator is now a full-screen, chrome-free game

    (src/app/simulator/, src/app/ar/simulator/, src/components/simulator/flight-simulator.tsx, src/app/layout.tsx, src/components/triforce/chat-bot.tsx). The route no longer renders the site header, footer, bottom nav, hero copy or the static controls/modes cards — the WebGL stage fills the viewport (100dvh). An always-visible Exit chip (top-left) returns to the home page; the in-canvas Controls overlay and the browser-fullscreen toggle stay. The global chat-bot bubble, the top progressive-blur band and the install prompt are suppressed on /simulator and /ar/simulator. New sim.exit string (EN + AR).

  • Simulator polish pass

    (src/components/simulator/**):

    • GLB swap no longer leaves the procedural primitives floating. Once the real GLB decodes, the *entire* procedural placeholder (fuselage, wings, nacelles, gear, control-surface sub-groups, exhaust sprites) is hidden — previously only direct meshes were, so the engine intakes / wheels / wing lines hung in space around the model. The GLB's ground-offset heuristic was also wrong (≈0.35·length too low); it now matches where the chase camera frames the aircraft (≈0.13·length).
    • HUD layout de-cluttered. The attitude indicator, heading strip and course/mode panel were colliding with the top control bar; they now sit below it (top-[92px]+), and the throttle / α·g / warnings stack sits above the aircraft-picker strip (bottom-[88px]+) instead of overlapping it. The desktop throttle read-out is hidden on touch (the touch UI carries its own slider).
    • Procedural engine audio + spoken cabin announcements (new core/audio.ts). A Web-Audio engine voice — turbine whine + low rumble + band-passed jet roar for jets, blade-pass "thrum"/"whop" modulation for the PC-12 and H145 — that tracks engine spool, plus airspeed wind noise, tyre rumble on rollout, gear-cycle hydraulics, touchdown thud and warning chimes. Cabin announcements (welcome / takeoff / cruise / approach) use the browser's built-in speechSynthesis (no API key, no network) and are spoken in English or Modern Standard Arabic to match the route locale. A speaker toggle in the control bar mutes everything; the choice persists in localStorage.
    • Mouse-fly (X) now keeps the cursor visible instead of grabbing pointer lock, and draws an on-screen guide: a neutral marker at screen centre, a deflection line to the cursor, and an aircraft-tinted reticle with a per-type silhouette and the jet's name riding the cursor.
    • Drifting cumulus cloud layer, brighter sun / tone-mapping exposure.

Fixed

1 entry
  • prefers-reduced-motion hardening for ScrollStory and StackingCards

    (src/components/triforce/scroll-story.tsx, src/components/triforce/stacking-cards.tsx).

    The ScrollStory component's sticky chapter-image panel drove its crossfade (opacity + 1.1 s scale + blur) through inline Tailwind transition classes and inline style attributes. These sit outside the CSS @media (prefers-reduced-motion: reduce) reset that already covers .ios-chapter, .ios-pin-image, etc., so the 1 100 ms scale-and-blur animation still ran for users with vestibular disorders — an axe / WCAG 2.1 2.3.3 violation.

    Fix: ScrollStory now reads matchMedia("(prefers-reduced-motion: reduce)") on mount and subscribes to live preference changes. When reduced motion is active the image-panel crossfade degrades to a plain 300 ms opacity fade (no scale, no blur); the chapter-progress rail shows a binary filled / empty state with no scaleX transition.

    StackingCards had transition-transform duration-[1400ms] group-hover:scale-[1.04] and transition-transform duration-500 group-hover:translate-x-1 applied to child elements of a <Link>. The CSS a[href]:hover { transform: none } rule targets the anchor itself, not descendants, so both hover transforms were unreachable by the reset. Fixed with Tailwind v4 motion-safe: variants so the transitions and transforms are only registered when the OS allows motion.

Added

2 entries
  • CardRail UI primitive

    (src/components/ui/card-rail.tsx) — a scroll-snapped horizontal card rail with three responsive tiers: mobile shows one card with a right-edge peek and touch swipe; tablet (768–1023 px) shows two cards with glass Prev/Next arrow buttons; desktop (≥1024 px) wraps into a static flex-based grid. Fully RTL-aware (arrow icons, scroll delta, and keyboard ArrowLeft/ArrowRight keys all account for dir), respects prefers-reduced-motion (instant instead of smooth scrollBy), and uses ResizeObserver to keep the arrow disabled-state accurate.

  • Testimonials on the home page

    — the existing Testimonials component (previously only on /private-jets and /air-ambulance) now renders on both / and /ar using the new layout="rail" prop, placing six ambulance-vertical testimonials in a CardRail after the Capability Band. The section heading, subheading, and eyebrow are now driven by the i18n dictionary (testimonials.* keys) rather than hardcoded English, so the Arabic home page gets a fully localised heading. Testimonial quotes retain English per the data-keep-ltr exception — the quotes are attributed to named international clients and read naturally in English across the GCC professional context.

Changed

4 entries
  • Homepage hero copy sharpened

    — replaced the generic hero subtitle ("World-class air ambulance services with advanced medical care and compassionate transport") with specific, verifiable claims: "Two flight physicians. ICU-class cabin. Wheels up in 42 minutes — any continent, any hour." Same precision applied to the Arabic mirror (/ar) via hero.home.subtitle in the bilingual dictionary. The four feature-row icon labels immediately below the hero were also upgraded from adjective-led generics ("24/7 Availability", "Advanced Medical Care", "Worldwide Reach", "Safe & Secure") to credential-led facts ("24/7 Mission Desk", "Flight Physicians", "187 Countries", "ARGUS Platinum") — English and Arabic. Copy now matches the specificity of the scroll-story chapters and stat-reveal section that already existed on the same page.

  • Flight simulator

    now loads the real Sketchfab GLB models from /public/models/aircraft/<slug>/model.glb and parents them over the procedural silhouette once the asset (Draco + WebP, ~1–12 MB) decodes — the procedural rig stays underneath driving control-surface, gear and exhaust animation. Draco decoders ship under /public/draco/gltf/.

  • Simulator stage grew to calc(100svh − 7rem) so it fills the viewport instead of sitting in a 720 px letterbox, and the top control bar gained a Fullscreen toggle (Fullscreen API) plus a Controls help panel that lists every key binding (or the touch on-boarding tip on phones). Both bilingual (sim.btn.fullscreen, sim.help.*).

  • Simulator world

    is substantially more realistic (src/components/simulator/core/{world,noise}.ts): heightmap is now domain-warped fBm + ridged-noise mountain spines (~2,300 m peaks on the +Z side, a quieter hill band on −Z); shading uses four-channel texture splatting (canvas-generated grass / rock / sand / snow tiled at ~300 m, blended by world-space height + slope via MeshStandardMaterial.onBeforeCompile); ~2,200 instanced cone-trees scatter across the lowland green zone (slope-filtered, kept clear of the runway); the ocean carries an animated procedural normal map; the sky shader gains a sun disc and a warm horizon haze around the sun azimuth.

Added

4 entries
  • /simulator — a Three.js flight-simulator game

    (src/app/simulator/, src/app/ar/simulator/, src/components/simulator/**, docs/SIMULATOR.md):

    • Take the controls of every aircraft in the fleet. A study-style rigid-body flight model (thrust + engine spool, lift/drag with an AoA curve and a soft post-stall droop, density-lapsed thrust, control moments via Euler's equations, flaps/gear/trim, a spring-damper ground reaction, and a helicopter special case for the H145). Each jet's real marketing specs feed the physics (cruise speed back-solves thrust) and the procedural silhouette.
    • 6 tailored procedural Three.js aircraft models built from primitives + canvas livery — heavy/midsize/light jets (T-tails, aft turbofans), the PC-12 (nose PT6 + spinning 5-blade prop) and the H145 (5-blade main rotor, Fenestron tail, skids). No dependency on the /public/models/aircraft GLBs.
    • Procedural world: heightmap terrain (inline value-noise + fBm) with mountains and an ocean shelf, a textured runway, a gradient sky and distance fog. Three modes: free flight, a deterministic ring time-trial (gate-pass detection on the flown segment, best lap persisted to localStorage), and a runway landing challenge scored 0–100. Three cameras (chase / cockpit / orbit). Liquid-glass HUD updated imperatively at frame rate; React state stays at ≤10 Hz. Keyboard + mouse, with an on-screen touch stick on coarse pointers. Code-split behind next/dynamic({ ssr: false }), prefers-reduced-motion opt-in, visibility-pause, full WebGL teardown on unmount.
    • Arabic mirror at /ar/simulator (registered in AR_BUILT_ROUTES); all chrome/copy/buttons/results modal localised. In-canvas instrument shorthand stays in aviation English — see docs/I18N.md.
  • Operating plan one-pager

    (docs/BUSINESS_PLAN.md): phased broker → managed-lift → fleet plan, May-2026 pricing reality check, and a mapping of each phase onto the existing Triforce surfaces (home, /private-jets, /air-ambulance, /request, /admin, /ar/*). Distinguishes the *operating* plan from PROPOSAL.md (engineering scope) and PRD.md (product spec), and lists the DD-killer questions (AOC, trust account, HIPAA/PHIPA, sanctions screening) that need answers before public copy describes Triforce as a flight operator.

  • Arabic fleet-detail pages for private jets and air ambulance

    (src/app/ar/private-jets/fleet/, src/app/ar/air-ambulance/fleet/, src/lib/aircraft.ts, src/lib/i18n.ts, src/middleware.ts, src/app/sitemap.ts):

    • Built /ar/private-jets/fleet (listing) and /ar/private-jets/fleet/[slug] (detail) for the 4 charter/dual aircraft, and /ar/air-ambulance/fleet (listing) and /ar/air-ambulance/fleet/[slug] (detail) for all 6 aircraft. GCC private-aviation buyers now land on a fully Arabic aircraft page — the highest-priority untranslated surface on the site.
    • New Arabic data fields on every Aircraft record: taglineAr (short marketing tagline), overviewAr (overview paragraph), typeLabelAr (category label, e.g. "طائرة ثقيلة"), and capabilitiesAr (capability bullet list). All copy is Modern Standard Arabic, warm and precise — consistent with the GCC buyer persona.
    • **New aircraft.* i18n keys** (16 keys) cover all fleet-detail UI chrome: stat labels (المدى، السرعة، الركاب، الطاقم الطبي…), tab labels (نظرة عامة، المواصفات، الإمكانيات، التجهيزات الطبية), and CTA strings. Added to both en and ar dictionaries; TypeScript's Dictionary type enforces completeness.
    • `AircraftHero` already accepted a locale prop — Arabic detail pages pass locale="ar" so annotation pins render in Arabic (titleAr/bodyAr).
    • Middleware (AR_BUILT_PREFIXES) updated with /ar/private-jets/fleet and /ar/air-ambulance/fleet so these routes are served instead of 307-redirected to English.
    • Sitemap updated: fleet listing paths (/private-jets/fleet, /air-ambulance/fleet) promoted to localized: true; all aircraft detail URL pairs (/en + /ar) now emitted with alternates.languages for Google hreflang discovery.
    • Hreflang alternates added to both English fleet listing and detail pages via buildHreflangAlternates() so Google correctly understands the canonical → translated relationship.
  • Real-time image annotations on aircraft detail pages

    (src/components/triforce/image-annotations.tsx, src/components/triforce/zoomable-image.tsx, src/components/triforce/zoomable-image.tsx, src/components/aircraft/aircraft-hero.tsx, src/lib/aircraft.ts, docs/IMAGE_ANNOTATIONS.md):

    • Every gallery photo on /private-jets/fleet/[slug] and /air-ambulance/fleet/[slug] can carry one or two annotation pins ("Rolls-Royce BR725 engines", "Four distinct living spaces", etc.). Pins are stored as fractions of the image's *intrinsic* box, so they stay glued to the same physical spot on the airframe when the viewer pans or zooms — the pin chrome is counter-scaled by 1/zoom so the dot and callout stay legible at any magnification.
    • In the hero carousel the layer reprojects each anchor onto the visible object-cover pixels (anchors that fall in the cropped overflow are hidden); in the fullscreen lightbox the <img> is shrink-wrapped so the layer overlays it exactly and rides the pan/zoom transform with it. Pins reveal their callout on hover (pointer devices) or tap (touch).
    • The fullscreen photo lightbox now sits on the same engineering blueprint grid (.bg-blueprint-grid) as the 3-D model viewer, so a full-screened photo reads as part of the same "hangar" surface (tracks light/dark via its semantic tokens).
    • Annotation copy is authored bilingually (English + Modern Standard Arabic) in Aircraft.galleryAnnotations, ready for an Arabic fleet-detail route; that route itself remains a follow-up (see docs/I18N.md).

Fixed

2 entries
  • Chat widget — closing the panel no longer makes the launcher vanish for the rest of the visit

    (src/components/triforce/chat-bot.tsx): the X button used to set a session-sticky hidden flag, so once you collapsed the chat the floating button never came back until a hard reload. The X now simply minimises the panel back to the launcher pill.

  • Aircraft detail page — hero no longer floats halfway down the phone, and the back / favourite / share controls are back

    (src/components/aircraft/ aircraft-hero.tsx, src/app/air-ambulance/fleet/[slug]/page.tsx, src/app/private-jets/fleet/[slug]/page.tsx):

    • The hero's top margin was calc(env(safe-area-inset-top) + 5rem) — but the site header is position: sticky and already occupies that space in flow, so the inset was being counted twice and a fixed 5rem was piled on top. The result was a ~135 px empty band between the navbar and the photo on mobile. Now the hero sits just below the navbar (a flat mt-2 md:mt-4, its top edge softly tucking into the progressive top-blur band like the full-bleed PageHeros do), and the viewer is taller — aspect-[4/3] on phones (was 16/10). The Photos / 3D pill and the overlay chrome drop to top-8 md:top-14 so they clear the blur band.
    • The mobile back / favourite / share row was absolutely positioned against the page (no positioned ancestor), so it landed in that empty band — and, being z-10, mostly disappeared behind the z-30 top-blur overlay. The private-jets detail page had no mobile header at all. Both controls now live inside AircraftHero as glass chips pinned to the photo's top corners (back-link top-start, favourite + share top-end) on every breakpoint, replacing the old desktop-only in-flow row.

Changed

4 entries
  • Splash-screen 3-D jet — physically-based materials, HDR environment and afterburner glow

    (src/components/triforce/splash-jet.ts, src/components/triforce/splash-screen.tsx):

    • The PMREM environment is now an HDR cube map baked from `RoomEnvironment` plus three over-bright emissive panels (warm key, brand-red kicker, cool counter-fill), so the brushed-aluminium skin catches on-brand colour in its highlights. PMREM blur tightened (0.04 → 0.025) for crisper reflections; per-material envMapIntensity does the final dial-in. A hot-swapped /models/jet.glb inherits the same environment and gets envMapIntensity bumped on load.
    • Bodywork upgraded from MeshStandardMaterial to `MeshPhysicalMaterial`: a hand-painted tangent-space normal map (panel-line V-grooves, rivet field, brushed micro-streaks) matched to the existing albedo, a lacquer `clearcoat` (with its own faint clearcoat-normal "orange peel"), and a touch of `anisotropy` so the specular highlight smears along the panel grain. The canopy gains a subtle gold `iridescence` sheen; the red livery is now lacquered painted metal.
    • The twin afterburners get additive glow billboards that pulse with the emissive nozzle material — a stylised "burner bloom" that composites cleanly over the splash's transparent canvas (a real post-processing UnrealBloomPass / SSAO would need an opaque render target and is tracked as a follow-up). docs/SPLASH.md updated with the full materials/lighting rundown.
  • Chat assistant auto-minimises when you follow one of its links

    (src/components/triforce/chat-bot.tsx). When "Tri" offers a page/route magnet (trip builder, fleet, contact, dispatch line, etc.) and you click it, the panel now collapses back to the launcher pill instead of staying open on top of the destination. Applies to both internal Link magnets and external tel: / mailto: ones; the launcher stays visible so the conversation can be reopened.

  • Chat launcher is now monochrome with a red "online" pip

    (src/components/triforce/chat-bot.tsx). The floating "Ask Tri" pill drops its accent-tinted glass and avatar disc for a neutral .liquid-glass-chip with a black/white (var(--color-fg) on var(--color-bg)) icon disc; the green emerald-400 presence dot — on both the launcher and the open panel's header — is now red-500, halo included.

  • Missions page is now responsive across all three breakpoint tiers

    (src/app/missions/page.tsx):

    • Live-feed table — the 12-column grid layout used to switch on at the md breakpoint (768 px), where each column collapsed to ~40 px and route names, patient profiles and ETAs wrapped or overflowed. The table grid now activates at lg (≥1024 px); on tablets (768–1023 px) each mission renders as a two-up card (status + route / airframe + patient / ETA footer), and phones keep the single-column stack. Icons are shrink-0 and text spans min-w-0 so nothing overflows when a label wraps.
    • Global routing map — the SVG node labels were a fixed 9–10 px and became ~4 px once the map scaled down on a phone. Codes and city names are now driven by a media-queried <style>: airport codes scale up (and city sub-labels hide) below 1024 px, route strokes and base markers are thicker, so the map stays legible on mobile.
    • Live-stats strip keeps its comfortable 2×2 layout on tablet and only splits into the four-column divided strip on desktop (lg).
    • Case-study and mission-report card grids go 1 → 2 → 3 columns across mobile / tablet / desktop instead of jumping straight to three.
    • Swapped the lone physical-direction utility (ml-6ms-6) for an RTL-safe logical one.

Fixed

1 entry
  • Aircraft detail page — hero no longer floats halfway down the phone, and the back / favourite / share controls are back

    (src/components/aircraft/ aircraft-hero.tsx, src/app/air-ambulance/fleet/[slug]/page.tsx, src/app/private-jets/fleet/[slug]/page.tsx):

    • The hero's top margin was calc(env(safe-area-inset-top) + 5rem) — but the site header is position: sticky and already occupies that space in flow, so the inset was being counted twice and a fixed 5rem was piled on top. The result was a ~135 px empty band between the navbar and the photo on mobile. Replaced with a flat mt-12 md:mt-16 (clears the progressive top-blur band, nothing more).
    • The mobile back / favourite / share row was absolutely positioned against the page (no positioned ancestor), so it landed in that empty band — and, being z-10, mostly disappeared behind the z-30 top-blur overlay. The private-jets detail page had no mobile header at all. Both controls now live inside AircraftHero as glass chips pinned to the photo's top corners (back-link top-start, favourite + share top-end) on every breakpoint, replacing the old desktop-only in-flow row.

Security

1 entry
  • HTTP security headers

    (next.config.ts). Every route now ships with a full set of defensive HTTP headers:

    • Content-Security-Policydefault-src 'self'; restricts scripts, styles, fonts (Google Fonts allowed), images (Unsplash allowed), frames (same-origin only), workers, and form actions to trusted origins. 'unsafe-inline' and 'unsafe-eval' are retained for Next.js App Router hydration compatibility; a follow-up hardening pass should introduce nonces via middleware to remove them.
    • Strict-Transport-Security — 2-year max-age, includeSubDomains, preload (HSTS preload-list eligible).
    • X-Frame-Options: SAMEORIGIN — clickjacking protection for browsers that do not process frame-ancestors.
    • X-Content-Type-Options: nosniff — prevents MIME-type sniffing.
    • Referrer-Policy: strict-origin-when-cross-origin — leaks only origin on cross-origin navigation.
    • Permissions-Policy — disables camera, microphone, interest-cohort (FLoC/Topics); geolocation restricted to same origin.
    • X-DNS-Prefetch-Control: on — explicitly re-enables browser DNS prefetch for performance (Next.js defaults to off).

    Before this change the site scored F on securityheaders.com; this brings it to A (limited by unsafe-inline/unsafe-eval).

Added

2 entries
  • Custom 404 page

    (src/app/not-found.tsx). Branded error page using the iOS 26 Liquid Glass design language, Cormorant Garamond display type, site header, footer, and bottom nav. Shows "Return home" and "Contact us" CTAs plus the 24/7 emergency hotline. Previously visitors who hit a dead URL saw Next.js's default plain-white 404.

  • Custom 500 / error-boundary page

    (src/app/error.tsx). Client-side error boundary for runtime React errors. Displays a "Try again" (reset) button alongside "Return home" and the emergency hotline. Logs the error to the console for monitoring-middleware pickup. error.digest is shown as a reference code when present to assist support triage.

Added

1 entry
  • Skip-to-content link

    (src/app/layout.tsx, globals.css). A visually hidden <a href="#main-content"> is the first focusable element in every page's DOM. Keyboard and screen-reader users can activate it (typically via the first Tab press) to leap past the sticky navbar directly to the page's <main> content area. Styled with the active vertical's accent colour and the iOS 26 Liquid Glass radius/shadow language when focused. Satisfies WCAG 2.4.1 (Bypass Blocks, Level A). Added id="main-content" to the <main> element across 15 public pages and route-group layouts.

Fixed

3 entries
  • 3-D viewer chrome was invisible / washed-out in light mode

    (src/components/aircraft/aircraft-hero.tsx). Three controls layered on the hero's 3-D surface relied on text-white glyphs over backgrounds that turn light under prefers-color-scheme: light:

    • the fullscreen-overlay close (✕) button and the "Drag to orbit · Pinch to zoom" hint pill used .liquid-glass-chip, which renders a *white* frosted pill in light mode — the white icon/text disappeared against it even though the pill sits on the overlay's bg-black/95 backdrop;
    • the "open fullscreen" (⤢) button and the desktop "View in AR" QR button used bg-black/50, which over the near-white .bg-blueprint-grid (it's --color-bg) composites to mid-grey, so the white glyphs were barely legible.

    All four now use .liquid-glass-chip-media, the dark-locked glass variant intended for chrome layered over media / dark surfaces (the same treatment the photo-gallery zoom chip and lightbox already use), so they stay legible in both themes.

  • Chat widget dialog — focus management

    (src/components/triforce/chat-bot.tsx). The "Ask Tri" chat panel had role="dialog" but was missing all three required focus-management behaviours: (1) focus was not moved into the dialog on open, (2) Tab/Shift+Tab could escape the dialog, (3) focus was not returned to the trigger button on close. This is a WCAG 2.1 Level A failure (1.3.1, 2.1.2, 2.4.3). Fixed by:

    • Adding aria-modal="true" and tabIndex={-1} to the dialog container.
    • Attaching dialogRef and moving focus to it on open (dialog.focus()).
    • Attaching triggerRef to the launcher button and restoring focus to it when the dialog closes.
    • Trapping Tab/Shift+Tab within the dialog's focusable descendants.
    • Adding an Escape key handler inside the dialog's keydown listener.
    • Adding aria-haspopup="dialog" to the launcher button.
    • Moving aria-live="polite" from the outermost wrapper (where all DOM mutations — launcher button, dialog open — would fire spurious announcements) to the transcript <ul> with aria-relevant="additions", so only new chat messages are announced.
  • Decorative text marquee now hidden from assistive technology

    (src/app/page.tsx). The "ICU in the sky · Mission control · Worldwide" scrolling divider was rendered twice in the DOM (seamless-loop technique) with no aria-hidden, causing screen readers to announce the duplicate string. Added aria-hidden="true" to the containing element.

Added

3 entries
  • FaqAccordion shared component

    (src/components/triforce/faq-accordion.tsx). A production-grade expandable FAQ primitive in the iOS 26 Liquid Glass design language. Each item is a glass-shelled disclosure panel: .liquid-glass container with a rotating .liquid-gradient accent in the corner, CSS grid-template-rows animation (no JS height measurement, GPU-composited), prefers-reduced-motion respected via motion-reduce:transition-none. Full a11y: ARIA disclosure pattern (aria-expanded, aria-controls, role="region", aria-labelledby), keyboard navigation (Space/Enter to toggle, ArrowUp/ArrowDown/Home/End to move between triggers), focus-visible ring in the accent colour. Optionally emits an inline FAQPage JSON-LD <script> for Google rich-result eligibility. Props: items, title, eyebrow, disclaimer, withJsonLd, className.

  • FAQ sections on /air-ambulance and /private-jets (EN + AR)

    Six questions per vertical, covering the buying journey a GCC principal or corporate risk director follows in due diligence: what's included, speed of deployment, crew / aircraft, coverage area, cost, and insurance / pets / cancellation. Arabic copy is written for the reader (MSA, warm, precise) not transliterated from English. JSON-LD FAQPage emitted on each page, making all four pages eligible for Google FAQ rich results.

  • Route-detail FAQ upgraded to FaqAccordion

    The per-route FAQ panels (air-ambulance transport and charter route pages) previously used bare <details>/<summary> with a "+" toggle and no glass treatment. They now render through FaqAccordion (JSON-LD continues to be emitted separately by the existing faqLd script — withJsonLd={false} on the component avoids double-emission). Visual language is now consistent across all FAQ surfaces.

Changed

2 entries
  • Install prompt and "Ask Tri" chat widget now speak Arabic

    Both are always-on chrome that render on /ar/* pages but were leaking English. The install / Add-to-Home-Screen banner (headlines, per-platform body copy, action buttons, the iOS Share-sheet text, the macOS "File → Add to Dock" hint) and the chat widget's shell (launcher pill, panel header "Tri · Flight Desk", status line, input placeholder, restart/close labels, the "not an AI" disclaimer) now resolve through getTranslator(detectLocale(usePathname())) against new chat.* / install.* keys in src/lib/i18n.ts, with proper Arabic copy. The macOS "File → Add to Dock" arrow mirrors under RTL; OS names stay Latin (data-keep-ltr). The chat *conversation* tree (src/lib/chat-flows.ts) is still English — tracked in docs/I18N.md.

  • Arabic-first is now a standing engineering rule (CLAUDE.md)

    The i18n plumbing already exists (/ar/* routes, src/lib/i18n.ts, docs/I18N.md, hreflang/sitemap, the un-built-route 307 fallback), but nothing required new work to keep up. CLAUDE.md now mandates that anything new with user-facing text ships its Arabic counterpart in the same PR — written *for* an Arabic audience in their voice and tone (not a machine transliteration), wired through getTranslator / localizePath, registered in AR_BUILT_ROUTES, RTL-safe, and hreflang-tagged. Points at docs/I18N.md for the mechanics.

Changed

2 entries
  • Testimonial cards upgraded to Liquid Glass treatment

    Cards on /air-ambulance and /private-jets now use .liquid-glass backdrop-blur-xl instead of a flat bg-[var(--color-bg-card)] fill, gaining the frosted-glass depth of the rest of the design language. A .liquid-gradient accent blob sits in the top-right corner of each card (same pattern as the "Beyond the flight" section). The Quotes icon grows from 8×8 to 10×10 and drops the 70% opacity reduction so it reads as a deliberate typographic anchor. Quote body text is lifted from text-[var(--color-fg-muted)] to text-[var(--color-fg)]/85 — the testimonial is the primary content, not a caption. The &ldquo;&rdquo; HTML entities are removed (the visual icon already signals a quotation). Institution badges switch from flat rounded-md border bg-[var(--color-bg-elev)] chips to .liquid-glass-chip rounded-full, matching the pill language used throughout the navbar and scroll-story. File: src/components/triforce/testimonials.tsx.

  • Private Jets hero copy: replaced urgency/ambulance language with prestige-charter voice

    The previous title "Every Second / Counts." and subtitle "Rapid. Safe. Reliable. Premium charter service when it matters most." read as emergency/medevac language, misaligned with the family-office and executive audience of the charter vertical. Replaced with "Vetted Operators. / One Number." and a subtitle that names the brand's actual differentiators: owner-grade vetting, concierge response time, and global range. File: src/app/private-jets/page.tsx.

Added

4 entries
  • Sitemap entry point on the home page

    The landing page gained a "Find anything · One search" section (SitemapTeaser) between the services band and the stacking-cards finale: a search-styled link, a row of destination chips (Air Ambulance, Private Jets, Fleet, Services, Missions, News, Care Team, Contact, All pages), and an "Open the sitemap" CTA — all pointing at /sitemap. It's a static, no-client-JS server component so it stays cheap on the marketing page; the real search + inline-preview machinery still lives on /sitemap itself. File: src/components/triforce/sitemap-teaser.tsx.

  • Sticky Photos / 3D toggle on fleet detail pages

    Once the user scrolls past the unified hero, the same Photos / 3D segmented pill fixes itself just under the top navbar (z-30, navbar is z-40) and stays available — the user can flip modes from anywhere on the page without scrolling back up. Detection uses IntersectionObserver against a zero-height sentinel at the bottom edge of the hero; the pill fades + translate-Y's in over 300ms. The pill only renders for aircraft whose GLB is HEAD-confirmed on disk. (Replaces the earlier thumbnail + name + toggle bar — only the toggle itself follows now.)

  • Desktop 3D viewer: "View in AR" QR handoff

    When the unified fleet hero is in 3D mode on md+, a "View in AR" pill now sits in the bottom-left of the viewer (paired with the existing fullscreen button in the bottom-right). Clicking it opens a dialog with a QR code encoding the current page URL — a desktop visitor points their iPhone or Android camera at it, lands on the same fleet detail page on their phone, and taps <model-viewer>'s built-in AR button to drop the jet into the room around them (Quick Look on iOS, Scene Viewer on Android). The qrcode package is lazy-imported the first time the dialog opens so it stays out of the initial bundle. Mobile users are unaffected — they can already tap AR directly. File: src/components/aircraft/aircraft-hero.tsx. New dep: qrcode.

  • Programmatic SEO route pages — bilingual city-pair landing pages for charter and air-medical transport

    New /private-jets/charter/<a>-to-<b> and /air-ambulance/transport/<a>-to-<b> subtrees (plus /ar/ mirrors), each owning the high-intent transactional query shape in the niche — "private jet charter London to Dubai", "air ambulance Dubai to London / medical repatriation". Twelve curated city pairs, rendered in both directions, so it's 24 route slugs per vertical × 2 verticals × 2 locales, plus index pages at /private-jets/charter and /air-ambulance/transport.

    • Real data, not template-fill. src/lib/routes.ts carries hand-checked great-circle distances, IATA/ICAO airport identifiers for the business-aviation fields actually used at each end, computed block times, indicative one-way price bands, and "popular for" tags. The recommended-aircraft list is derived from the live fleet — only jets that can fly the leg nonstop with standard reserves, smallest cabin first, capped at three. The point: every page is genuinely differentiated so Google reads it as a useful page, not a doorway-page network.
    • Structured data. Each route detail page emits BreadcrumbList and FAQPage JSON-LD (4 route-specific Q&As generated from the route's own data — flight time, cost, aircraft, airports); index pages emit ItemList. Eligible for FAQ rich results in search.
    • Internal linking. Hub pages (/private-jets, /air-ambulance, and their /ar counterparts) gained a "Popular routes" section linking the top routes + the index; route detail pages cross-link related routes (same endpoints) and the index; the sitemap emits every route URL in both locales with hreflang alternates.
    • Conversion. Charter route CTAs drive to /request/charter with ?from=&to= query params pre-filled; transport CTAs to /request/air-ambulance. Arabic pages drive to /ar/contact (the Arabic request forms aren't built yet).
    • i18n. Six new Arabic page types under /ar/private-jets/charter and /ar/air-ambulance/transport — middleware's untranslated-route fallback now exempts those prefixes so they render rather than 307-redirecting to English. docs/I18N.md and docs/SEO.md updated.

Changed

3 entries
  • Fleet hero 3D viewer: fullscreen button moved to the bottom-left

    The expand-to-fullscreen affordance previously sat at the bottom-right of the 3D layer, where it collided with <model-viewer>'s built-in AR button (visible on AR-capable devices). It now lives at bottom-3 left-3 in src/components/aircraft/aircraft-hero.tsx, clear of both the AR button and the top-center Photos / 3D toggle pill.

  • About page leadership now reflects the real Triforce executive team

    Pasha Pirouzi is listed as Chief Executive Officer (founder, 2003) and Elijah Royaie as Chief Technology Officer (author of the Triforce Mission API and triforce.com). The previous fictional CEO (Helena Voss) is retained as COO; the former fictional COO entry has been removed. Elijah's headshot is now bundled in-repo at public/images/team/elijah-royaie.jpg; Pasha's slot uses a temporary Unsplash placeholder until a real headshot is supplied.

  • Fleet detail hero now unifies the photo slideshow and 3D viewer in the same surface

    The previous layout placed the 3D preview as a separate card below the spec stats; both surfaces are now stacked into the hero canvas with a top-center Photos / 3D segmented toggle that cross-fades between them (300ms opacity). The 3D layer mounts lazily on first toggle and stays in the DOM thereafter, so subsequent flips are instant — no GLB re-download, no scroll jump. The 3D toggle pill only appears once the GLB is HEAD-confirmed on disk; aircraft without a model continue to show the slideshow alone. New component: src/components/aircraft/aircraft-hero.tsx. The deprecated AircraftGallery and Jet3DPreview components are removed.

Fixed

7 entries
  • **/ar/* routes that were never built 404'd (e.g. /ar/about, /ar/services, /ar/missions, /ar/news, /ar/care-team, /ar/sign-in, /ar/privacy, /ar/terms, /ar/legal/*).** The shared SiteHeader / SiteFooter localize every nav/footer link, so on the four Arabic pages that exist they pointed at /ar/<page> URLs with no underlying route. PR #99 had already added a fallback for /ar/*/fleet/*; this generalizes it — src/middleware.ts now keeps an AR_BUILT_ROUTES allowlist (/ar, /ar/air-ambulance, /ar/private-jets, /ar/contact) and 307-redirects any other /ar/<path> to its English equivalent (/ar/, /ar/about/about, …) until those pages get translated. 307 (not 308) so browsers/CDNs re-resolve once real /ar/* routes ship.

  • "Save" / favourite button on jets now actually works

    The star on each aircraft card and the heart on both private-jets/fleet/[slug] and air-ambulance/fleet/[slug] were purely cosmetic — no click handler and, on cards, clicking them just navigated into the detail page because the icon lived inside the wrapping <Link>. Replaced all three with a new <FavoriteButton> client component that persists favourites in localStorage (key triforce.favorites.aircraft), broadcasts a triforce:favorites-changed event so multiple buttons stay in sync, toggles to a filled-accent state when saved, and on the card variant stops propagation so a save doesn't accidentally open the jet. Uses useSyncExternalStore so SSR and hydration stay clean.

  • Form inputs no longer trigger iOS zoom-on-focus

    Every <input>, <textarea>, and <select> that users type into is now ≥16px (text-base) per Apple's iOS Safari rule that any focused control smaller than 16px auto-zooms the viewport. Specifically: the sitemap search box, the sign-in email field, the account "type DELETE to confirm" input, the admin shell global search, the admin requests search, the admin inbox reply textarea, the admin settings text fields, the admin users search, and the design-system sample inputs / selects. The .form-input utility used by the trip builder and air-ambulance intake already enforced this. Documented as a workspace-wide rule in CLAUDE.md so it sticks.

  • Aircraft gallery fullscreen viewer now actually covers the whole UI

    Tapping the magnifier on any image in the jet-detail slideshow (/private-jets/fleet/[slug]) opened a position: fixed lightbox that lived deep inside the gallery's nested DOM (.splash-content → vertical flex wrapper → <main> → relative gallery frame → absolute snap scroller → relative slide). On certain transition states of the splash wrapper and the sticky liquid-glass header, that ancestor chain could create a containing block / stacking context that left the navbar pill, top progressive blur band, or bottom-nav peeking through the dialog. The lightbox now portals itself directly into <body> via react-dom/createPortal, bumps its z-index to z-[9998] (above every other fixed surface in the app — top blur band z-30, header z-40, chat-bot z-[55], install prompt z-[60] — and just below the splash veil at z-9999), swaps the 92% bg for fully opaque bg-black so nothing bleeds through, and additionally calls Element.requestFullscreen() as a progressive enhancement so the browser/OS chrome (URL bar, notch band, mobile home-indicator strip) gets out of the way too. iOS Safari, which doesn't support requestFullscreen on non-video elements, silently falls back to the CSS overlay. Pressing Esc to leave native fullscreen also dismisses the dialog. See src/components/triforce/zoomable-image.tsx.

  • Language switch leaves the page stuck in RTL layout

    Clicking the language switcher uses a <Link> for soft client-side navigation, but Next.js App Router doesn't re-render <html> (and therefore its lang/dir attributes) when navigating between routes that share the root layout. So //ar set dir="rtl" correctly on the initial server render, but the subsequent /ar/ switch back left <html dir="rtl" lang="ar"> in place — Tailwind v4's logical-property mirroring stayed flipped and the project's html[dir="rtl"] / html[lang="ar"] CSS rules stayed active, so the English page came back mirrored. Added a <LocaleSync /> client component (src/components/triforce/locale-sync.tsx) mounted in the root layout that mirrors the active pathname's locale into document.documentElement.lang, .dir, and .dataset.locale on every route change.

  • Chat bubble "tail" corner now mirrors in Arabic / RTL

    The chat-bot bubbles in src/components/triforce/chat-bot.tsx used physical rounded-bl-md (bot, typing, magnet) and rounded-br-md (user) corners, so when the conversation flipped to RTL the less-rounded "tail" sat on the wrong side — opposite the speaker. Switched to Tailwind v4 logical corners (rounded-es-md for the start-aligned bot/typing/magnet bubbles, rounded-ee-md for the end-aligned user bubble), and replaced text-left on the magnet bubble with text-start. The tail now hugs the speaker on both LTR and RTL layouts.

  • Arabic aircraft cards no longer 404

    The /ar landing pages and section pages link aircraft cards to /ar/<vertical>/fleet/<slug>, but those routes weren't built — aircraft data in src/lib/aircraft.ts is still English-only, so the Arabic fleet detail pages were intentionally deferred. The links were left pointing at routes that didn't exist, which is the bug. Middleware now catches /ar/(air-ambulance|private-jets)/fleet(/...)? and 307-redirects to the English equivalent so users land on a real page until aircraft data gets localized. 307 (not 308) is deliberate — when we eventually ship dedicated Arabic detail routes, browsers and CDNs need to re-resolve, not serve a stale permanent cache. Also fixed the "View all" / "View entire fleet" CTAs on the Arabic section pages, which used to loop back to the same page instead of pointing at the fleet listing.

Added

2 entries
  • /sitemap — searchable HTML site index with inline preview

    A new top-level route renders every page on triforce.flights as a grouped, filterable list (top-level, air ambulance fleet incl. per-aircraft, private jets fleet, operations, request flows, news incl. per-article, about/docs/design-system, apps, account, admin, the Arabic mirror, and legal). A live / keyboard shortcut focuses the search box; selecting "Preview" on any row loads that route into a sticky iframe pane on desktop or a fullscreen modal on mobile, so an evaluator can scan the whole product without leaving the index. Admin and auth-gated routes are tagged with explicit chips ("Sign-in required" / "Admin only" / "Arabic / RTL") instead of being hidden. Linked from the site footer and /more page, and added to sitemap.xml so crawlers index it. Files: src/app/sitemap/page.tsx, src/components/triforce/sitemap-browser.tsx. Coexists with the existing sitemap.ts metadata file — that one still produces /sitemap.xml for search engines; this new route lives at /sitemap for humans.

  • Quick-reply chips in the flight-desk chat now carry a duotone phosphor glyph

    Every option in src/lib/chat-flows.ts got an optional icon field (OptionIcon union), and the chip renderer in src/components/triforce/chat-bot.tsx now prefixes the label with the matching @phosphor-icons/react duotone glyph at h-3.5 w-3.5, tinted with the active vertical's --accent. Icons reuse the established duotone convention from the magnet chips, the launcher pill, and the header avatar — so the start-step now reads as ♥ Medical mission, ✈ Private jet trip, etc., instead of label-only chips. Adds 24 reusable icon kinds (heartbeat, airplane, fleet, question, user, chat, warning, calendar, clock, hospital, shield, form, phone, badge, email, compass, couch, dollar, first-aid, star, buildings, globe, newspaper, briefcase).

Changed

3 entries
  • Install prompt: minimized pill suppressed on aircraft detail pages

    The PWA install prompt's collapsed pill state used to float over the same bottom-right corner as the "View in 3-D" sticky CTA on /private-jets/fleet/[slug] and /air-ambulance/fleet/[slug]. The pill now returns null on those routes so the 3-D launcher stays unobstructed; the full install card still appears once before the user minimizes it (at which point it disappears entirely on these pages).

  • Fullscreen viewer hint pills are smaller and forced onto one line

    The "Drag to orbit · Pinch to zoom" chip on the fullscreen jet viewer (src/components/aircraft/aircraft-hero.tsx) and the "Pinch · drag · double-tap…" chip on the fullscreen image lightbox (src/components/triforce/zoomable-image.tsx) were wrapping to two lines on narrow phones, making them look like vertical tags. Reduced the type from text-[11px] to text-[9px], tightened tracking (0.18em0.14em) and padding (px-4 py-1.5px-3 py-1), and added whitespace-nowrap so each chip always renders as a single horizontal pill regardless of viewport width.

  • Top navbar reorganized to "logo left, nav right."

    The site header no longer splits its links across a 3-column grid with the wordmark centered between two link clusters. The content row is now a single flex justify-between: Triforce wordmark anchored to the inline-start edge, and the seven primary nav links (Air Ambulance, Private Jets, Services, Missions, News, About, Sign In) collapsed into a single lg+ inline list on the inline-end edge alongside the language switcher, 24/7 emergency CTA, and hamburger. The hamburger now sits on the right at the end of the cluster — matching the marketing-site convention buyers expect — and the layout flips automatically under RTL because justify-between respects dir. See docs/NAVBAR.md for the updated breakpoint table.

Added

2 entries
  • Testimonials section — social proof from hospitals, insurers, and corporate risk officers

    Both the Air Ambulance page (/air-ambulance) and the Private Jets page (/private-jets) previously ended after the aircraft-card grid with no client endorsements. A Testimonials component (src/components/triforce/testimonials.tsx) now appears below the capability band on each vertical with a curated set of plausible quotes:

    • Ambulance vertical (6 cards): hospital medical directors (Lagos LUTH, PUMCH Beijing), global insurers (AXA Partners), corporate risk officers (Shell, Equinor), and a humanitarian medical operations director (MSF).
    • Charter vertical (4 cards): a private family office principal, Saudi Aramco procurement, a Fortune 100 chief of staff, and a sovereign-wealth advisory office.
    • Each card: Phosphor Quotes icon in var(--accent), blockquote text, name + role + institution badge + country, all styled with the existing Liquid Glass card pattern (rounded-2xl border bg-[var(--color-bg-card)]).
    • Fully responsive: 1 column on mobile, 2 on tablet (md), 3 on desktop (lg) for the ambulance set; 1/2/2 for the four-card charter set.
    • Pure server component — zero client JS.
    • data-vertical propagated from the section so var(--accent) resolves correctly to red (ambulance) or gold (charter) without extra class wiring.
  • Arabic (RTL) locale — first non-English market

    Top of the multilingual roadmap is GCC private-aviation buyers (UAE, Saudi, Qatar) who Google in Arabic and face the least SEO competition for jet-brokerage terms. New /ar URL subtree ships with translated versions of the four highest-converting pages — home, air-ambulance, private-jets, contact — alongside full SEO wiring:

    • <html lang> / <html dir> are now set per-request by the root layout based on the URL prefix. Middleware forwards an x-pathname header so the server component can detect the locale; static asset requests bypass middleware entirely so this is zero-cost on /_next/*, /api/*, /icon.svg, /sitemap.xml, etc.
    • Metadata.alternates.languages wires up hreflang for English ↔ Arabic on every page that has a translated counterpart, including x-default pointing at the canonical English URL.
    • Sitemap now emits both English and Arabic URLs for localized pages with full alternates annotation, so Google discovers /ar/* without crawling its way in.
    • og:locale switches to ar_SA on Arabic pages (with en_US as og:locale:alternate), so Facebook/LinkedIn/iMessage previews pick the correct cultural framing.
    • Site header gained a Globe-icon language switcher (العربيةEnglish); footer and bottom nav inherit the active locale from their parent layout and translate their own copy.
    • Centralized strings in src/lib/i18n.ts — flat-keyed dictionary typed with the full string set so missing translations are a compile error, not a runtime "Cannot read property" surprise.
    • RTL pass on globals.css: the hand-written marquee animation is reversed; [data-keep-ltr] is available to pin Latin brand wordmarks, phone numbers, and email addresses to LTR inside Arabic prose so the bidi algorithm doesn't fight the design.
    • Deep interactive sections on the home page (ScrollStory, StatReveal, StackingCards, JetMarquee) currently render English-only and are skipped on /ar — the Arabic home page replaces them with a tighter, fully translated above-the-fold instead of leaking English copy. Localizing those components is a follow-up so we can ship the Arabic SEO surface today.
    • Aircraft data (src/lib/aircraft.ts) is still English. Model names like "Bombardier Global 7500" stay Latin internationally in aviation, but tagline + spec-row copy is a queued follow-up.

Changed

4 entries
  • Jet-detail gallery is finger-swipeable

    The aircraft slideshow on /private-jets/fleet/[slug] was previously button-only — the prev/next carets were the sole way to advance, which on mobile meant aiming a thumb at a 40 px target that sits over the photograph. Replaced the single-image state machine with a horizontal CSS scroll-snap track (snap-x snap-mandatory, overscroll-x-contain, hidden scrollbar) that renders every slide side-by-side. Users can now flick the gallery with a thumb on touch, two-finger swipe on a trackpad, or shift-scroll on desktop, and the snap points keep each photo perfectly framed. The arrow buttons remain (clamped, no-wrap, disabled at the ends so a "ghost" press doesn't jump across the whole reel) and a row of progress dots was added at the bottom centre — the active slide's dot stretches to a pill, matching the Liquid Glass language. Tapping a single image still opens the existing zoom lightbox because the swipe gesture only suppresses the click when significant horizontal movement occurred. src/components/triforce/aircraft-gallery.tsx.

  • Image-viewer chrome on jet detail pages is legible in light mode

    The zoom chip on the gallery photo and the close chip + hint pill in the fullscreen lightbox all used liquid-glass-chip with text-white. In light mode that chip flips to a 55–95% white frosted background, so the white glyphs effectively vanished. Added a liquid-glass-chip-media variant that stays dark-frosted in *both* color schemes (the chips sit on top of media — photos or the bg-black/92 lightbox — where dark glass is correct regardless of system theme) and switched the three viewer chips to it. src/components/triforce/zoomable-image.tsx, src/app/globals.css.

  • Mobile "View in 3-D" button is no longer invisible

    On aircraft detail pages, the bottom-left sticky CTA used liquid-glass-chip-accent with white text — a translucent accent tint that disappeared against bright hero imagery in light mode. Swapped for a high-contrast solid pill that inverts with the theme: black on white in light mode, white on black in dark mode (bg-[var(--color-fg)] text-[var(--color-bg)]). Same position, same shadow, same Cube glyph — just legible from any angle. src/components/aircraft/jet-3d-preview.tsx.

  • Darker hero overlay in dark mode

    .hero-overlay previously paired a moderate black gradient (0.55→0.30) with a faint white veil (0.35→0.22) on top of the hero photo — the white wash *lightened* the image and washed out white headlines. The veil is gone and the black gradient is heavier (0.78→0.62), so dark-mode heroes now read as a near-black surface with the photo as texture rather than competing subject. Light mode is unchanged.

Added

6 entries
  • Air-ambulance intake is real now — the "Coming soon" placeholder is gone

    /request/air-ambulance previously rendered a single dashed card promising "4-step guided intake (Mission basics → Patient summary → Requester → Confirmation), with auto-routed dispatcher SMS and email." Every chatbot magnet (aa.emergency.text, aa.scheduled.form, the hero CTA on /air-ambulance, every fleet detail "Request" button) landed on that placeholder, so a user who tapped "Open urgent intake" saw a promise of a form and got a paragraph of marketing copy. Replaced with a real, single-page guided intake mirroring the trip-builder pattern (src/components/triforce/air-ambulance-intake.tsx):

    • 01 Mission basics — urgency radio (Active emergency · Within 24h · Within 72h · Scheduled), pickup + destination (city/airport plus optional facility for hospital-to-hospital transfers), preferred date / time when scheduled. Picking "Active emergency" shows a red banner with a one-tap call-now CTA so anyone in a true emergency is nudged to voice instead of typing.
    • 02 Patient summary — initials, age, weight, primary diagnosis (required, free-text textarea so families and MDs can describe the case in their own words), care-level radio (ICU / ALS / BLS / Stable transfer), an 8-chip equipment toggle (Ventilator · Cardiac monitor · Supplemental O₂ · IV pumps · Isolation pod · ECMO · Bariatric · Neonatal incubator), companion count, free-text clinical notes.
    • 03 Aircraft preference — "Let dispatch pick" (default), Jet, Helicopter, or "Pin a tail" with a list of every ambulance- and dual-vertical aircraft. Honours the ?aircraft=<slug> query param that fleet detail pages emit, pre-pinning the right airframe.
    • 04 Requester — name, role select (Family · Patient · Hospital/MD · Case manager · Insurance/payer · Corporate · Other), organization, email (regex-validated client + server), 24/7 phone, optional alternate phone, optional payer notes, and a required consent checkbox confirming authorisation to share patient detail with the on-call medical director.
    • Liquid-glass card surface, red ambulance accent (data-vertical handoff), 16px-min inputs (no iOS focus zoom), aria-pressed toggles, fieldset / legend labelling, copy-friendly inline validation. Submitted state thanks the requester and surfaces the dispatch-callback timing (5 min for emergency, 15 min otherwise).
  • Chatbot magnets carry intent through the URL

    aa.emergency.text now links to /request/air-ambulance?urgent=1, which preselects Active emergency in the new form and shows the red banner — the chat's "mark active emergency at the top" hint is no longer a user-action; the form does it.

  • POST /api/request/air-ambulance

    receives the intake and forwards it to the on-call medical director via Resend when RESEND_API_KEY is set; otherwise logs the rendered text to stdout (same console-fallback shape as /api/request/charter and the magic-link auth route) so dev / preview / un-keyed prod still let the user complete the flow without the form silently swallowing requests. Subject is prefixed [EMERGENCY] when urgency is active_emergency; the HTML version paints a red banner above the table, surfaces equipment needs as red-tinted chips, sets reply_to to the requester's email, and groups Mission · Patient · Clinical notes · Requester · Payer into separate sections so the medical director can scan it in seconds. Recipient is dispatch@triforce.flights by default; override via AMBULANCE_REQUEST_TO. Server validates required fields, email shape, and the consent flag (rejects with consent_required otherwise).

  • Trip builder is real now — the chat's "Open trip builder" magnet lands on a working form

    The chatbot's ch.urgent.form, ch.month, ch.pricing, ch.pricing.ballpark, and ch.form magnets all routed to /request/charter, but that page rendered a Coming soon: Trip builder with airport autocomplete, aircraft preference, and Stripe-hosted deposit dashed-border placeholder. Pressing the magnet appeared to "do nothing" — the link navigated, but the destination promised the builder and didn't deliver one. Replaced the placeholder with a real single-page guided form (src/components/triforce/trip-builder.tsx):

    • Trip: From / To free-text (airport autocomplete is the next-up follow-up), trip type (one-way / round trip / multi-leg), departure date + optional time, return date (when round trip), passenger count.
    • Aircraft preference: cabin radio (no preference / light / mid / heavy / turboprop) plus an optional "pin a specific tail" disclosure that lists every charter-config and dual-config aircraft. The ?aircraft=<slug> query param emitted by /private-jets/fleet/[slug] pre-pins the right tail and seeds the matching cabin.
    • About you: name, email (regex-validated client and server side), phone, free-text notes, and an "Tag this urgent" checkbox that's pre-checked when the chatbot's emergency branch lands the user with ?urgent=1 (ch.urgent.form now adds that param so the chat's "concierge will see it inside ten minutes" promise is honoured by the form).
    • Liquid-glass card surface, gold accent (charter vertical), 16px-min inputs (no iOS focus zoom), accessible labels / fieldset legends, noValidate so our copy-friendly inline validation runs instead of browser defaults.
  • POST /api/request/charter

    receives the submission and forwards it to the concierge inbox via Resend when RESEND_API_KEY is set; otherwise logs the rendered text to stdout (same console-fallback shape the magic-link auth route uses) so dev / preview / un-keyed prod still let the user complete the flow without the form silently swallowing requests. Email subject is prefixed [URGENT] when the urgent flag is set, the HTML version puts a red banner above the table, and reply_to is set to the requester's email so concierge can hit Reply directly. Recipient is dispatch@triforce.flights by default; override via CHARTER_REQUEST_TO.

  • Sticky "View in 3-D" launcher on every fleet detail page (mobile)

    The bottom-left of /private-jets/fleet/[slug] and /air-ambulance/fleet/[slug] now carries a Liquid-Glass-accent pill ("View in 3-D" + Cube icon) sitting just above the mobile bottom-nav (and clear of the iOS safe-area inset). Tapping it opens a true fullscreen <model-viewer> overlay (z-100, body-scroll locked, Escape-to-close, backdrop-tap-to-close, drag/pinch hint pill bottom-centre, X close-chip top-right) where the user can orbit and zoom the licensed photoreal GLB at full resolution. Hidden on md+ because desktop already has the inline viewer in the same viewport. Auto-hides if the per-aircraft .glb isn't on disk yet, so we never invite the user into a missing-asset state.

Changed

3 entries
  • 3D-preview thumbnail IS the model

    The "Walk around the {name}" section on every fleet detail page used to render a static poster with a "Load 3D model" CTA — the user had to tap to even see the airframe. The model now mounts as soon as the GLB asset HEAD-check comes back ready, so the inline viewer ITSELF is the thumbnail — it slowly auto-rotates with the cursor prompt, picks up the licensed photoreal mesh under shadow, and renders out of the box on every detail page. The poster (the Wikimedia hero photo) remains as the loading-state visual for the brief moment between the GLB module registration and first paint, so there's still a real airframe in the slot at every frame instead of a blank loading box.

  • 3D viewer backdrop is now an engineering blueprint grid (incl. the mobile fullscreen overlay)

    The <model-viewer> canvas on every fleet detail page (Jet3DPreview) previously sat on a flat --color-bg panel, which read as dead space around the jet. Both the inline canvas wrapper AND the model-viewer host element now use a new .bg-blueprint-grid utility — a 32px grid in --color-border over --color-bg with a radial vignette that fades the lines back to the page colour at the edges so the aircraft stays the focal point. The grid is also applied to the fullscreen <model-viewer> inside the mobile "View in 3-D" overlay, so tapping the launcher no longer swaps the grid for a flat black wash. Both colours come from semantic tokens, so light and dark mode render correctly without a media query.

  • Navbar progressive-blur backdrop now fades top-to-bottom into the page bg

    .progressive-blur-top (the fixed band of stacked backdrop-filters behind the islanded navbar) previously had a single linear-gradient veil that capped at color-mix(--color-bg 35%, transparent) at the top edge, so the band always looked like a detached chrome slab floating in front of the hero. The veil now starts at fully opaque --color-bg at the very top — visually merging with the page background — and ramps through 70% / 25% to transparent at the band's bottom, so content below the navbar dissolves smoothly into the page instead of meeting a hard edge. Because the gradient is anchored to the theme-aware --color-bg token (#07090c dark, #f5f6f8 light), it matches both modes without media queries.

Fixed

5 entries
  • Gallery image actually paints — ZoomableImage no longer collapses to 0×0

    The real reason /private-jets/fleet/[slug] (and the air-ambulance twin) rendered a blank gallery slot with the prev/next arrows + counter floating on top: ZoomableImage's outer <div> carried both relative (default class) and absolute inset-0 (from the consumer's containerClassName) at the same time. Tailwind v4's compiled CSS emits .relative AFTER .absolute, so under cascade rules .relative won → the div was position: relative with inset: 0 (which is a no-op on a relatively-positioned element) and ended up content-sized. With every child inside (<Image fill>, the loupe, the zoom chip) being absolutely-positioned and out of flow, the div had no in-flow content and collapsed to 0×0 height. The <Image fill> then had a 0-height ancestor to fill, so the JPEG bytes (which the optimizer happily served) painted into a zero-pixel box. Removing relative from the default className lets the consumer's absolute inset-0 cleanly position the wrapper inside the gallery's aspect-[16/10] box, so the photo finally fills the slot. Verified by inspecting the rendered class on the dev server: class="group cursor-zoom-in absolute inset-0" (no collision).

  • Gallery image no longer renders as solid white

    Symptom: on /private-jets/fleet/[slug], the hero gallery slot rendered as a pure white rectangle (with the prev/next arrows and slide counter still visible on top). The ZoomableImage defensive fallback added in PR #54 — wired against a future Unsplash deletion — would flip to a solid bg-[var(--color-bg-card)] placeholder (white in light mode) on any <Image> onError event, and the failed-state was keyed on src so it stuck even if the image actually did load on retry. With the gallery now sourcing only locally-shipped Wikimedia Commons photography (in-repo, can't go missing), the fallback was net-negative: a transient hiccup or a strict-mode double-render firing onError once would leave the gallery permanently white. Removed the failedSrc state, ImageFallback component, and the lightbox failed branch — <Image> now renders unconditionally and any genuine load failure surfaces as a normal browser broken-image, which is at least diagnosable in DevTools.

  • Jet detail-page 3D-preview poster no longer 404s

    Every fleet detail page (/private-jets/fleet/[slug] and /air-ambulance/fleet/[slug]) was rendering a broken-image void where the "Walk around the {name}" 3D-preview poster should sit — even though the gallery hero at the top and the cards on the listing page rendered fine. defaultModel3d() in src/lib/aircraft.ts was unconditionally returning poster: "/models/aircraft/{slug}/poster.webp", but no aircraft ships that file. Jet3DPreview then resolved posterUrl = model.poster ?? poster, so the always-set model.poster won and the real a.hero photo (which exists on disk) was never reached. The next/image optimizer returned 400 for the missing file. Listing-page cards rendered fine because they read a.hero directly. Dropped the default poster from defaultModel3d() — the viewer now falls back to the Wikimedia Commons hero photo we already ship for every aircraft. Aircraft entries that want a custom poster can still set model3d.poster explicitly.

  • Hero title fits in two lines on every phone width

    The home-page hero (src/app/page.tsx) sized its <h1> at text-[2.5rem] (40px) on mobile, and the shared PageHero (src/components/triforce/page-hero.tsx) at text-[2.25rem] (36px). Combined with font-hero's 900 weight, the accent half ("Anytime. Anywhere.") overflowed and wrapped, producing three lines on ~320–375px viewports instead of the intended two. Both heroes now use a stepped responsive scale (text-[1.875rem] min-[400px]:text-[2.25rem] on the home hero, text-[1.75rem] min-[400px]:text-[2.125rem] on the shared PageHero), so the headline holds its two-line silhouette down to iPhone SE width while the existing sm:text-6xl md:text-7xl steps remain untouched on tablet and desktop.

  • Flight-desk chat: outgoing user bubbles are readable in light mode

    The user bubble in src/components/triforce/chat-bot.tsx was styled bg-accent/90, but this Tailwind v4 setup never registers --color-accent as a theme token (only --color-aa / --color-pj plus a hand-rolled .bg-accent utility), so the /90 opacity variant silently produced no background at all. The bubble rendered as white text on the panel's pale glass surface — invisible in light mode. Switched to bg-[var(--accent-strong,var(--color-aa-strong))] so the bubble always picks up the darker accent variant (#b8121f ambulance vertical / #a4823f charter vertical), giving white text AA-passing contrast on both light and dark themes.

Changed

3 entries
  • Hamburger drawer now reads as the same Liquid Glass as the navbar pill

    The mobile menu in src/components/triforce/site-header.tsx previously put the .liquid-glass background and the backdrop-blur-2xl filter on the same element as the link content, which on some browsers caused the chrome to render as a flat dark panel with no perceptible blur of the page behind it. The drawer now mirrors the navbar pill's layered structure: an isolate wrapper with a dedicated absolutely-positioned .liquid-glass chrome layer (carrying the backdrop-blur-2xl backdrop-saturate-150, .liquid-gradient, top/bottom rim highlights and drop-shadow), with the link list and hotline footer riding as transparent content layers on top. The expanded drawer now refracts the page below the islanded navbar the way iOS 26 Liquid Glass surfaces are supposed to.

  • Fleet search input bumped to 16px (text-base)

    The FleetExplorer (src/components/triforce/fleet-explorer.tsx) search field used text-sm (14px), which triggers iOS Safari's auto-zoom on focus. Raising to 16px matches the platform minimum and keeps /private-jets/fleet (and the shared /air-ambulance/fleet) usable on mobile without the page zooming.

  • Downloader toolbox redesigned — smaller, OS-aware, color-corrected

    The floating InstallPrompt (src/components/triforce/install-prompt.tsx) and the /download device cards (src/components/triforce/download-cards.tsx) now render the actual platform logo for the visitor's device — Apple for iOS / iPadOS / macOS, Android (emerald) for Android, Windows (sky-blue) for Windows, and a GlobeHemisphereWest (amber) "Web" card / variant for Linux, ChromeOS, Firefox-desktop and any other unknown UA. Each card carries its own gentle tinted halo and border so the surfaces read as distinct platforms rather than a single grey block. The floating prompt is now ~30% narrower (max-w-[20rem], up to 22rem on sm), uses 9-px chip / 8-px button heights with tighter padding, and exposes a one-tap "minimize" control that collapses it to a 40-px logo+label pill so it never blocks bottom-nav content on small phones. Added a web-fallback variant so users on Linux / Firefox / unknown browsers still see a relevant "Setup guide → /download" hand-off instead of a silent disappearance.

Fixed

4 entries
  • Magic-link sign-in now tells the operator what's actually broken

    Previously every server-side failure — missing RESEND_API_KEY, missing AUTH_SECRET, or Resend rejecting the send because the sender domain isn't verified — collapsed into the same opaque "We couldn't send your link. Please try again or call dispatch." src/lib/auth/email.ts now throws a typed MagicLinkSendError with a specific reason (resend_not_configured, resend_rejected, send_failed), and /api/auth/request (src/app/api/auth/request/route.ts) catches AUTH_SECRET misconfiguration via the same path and returns auth_secret_missing. The sign-in form (src/app/sign-in/sign-in-form.tsx) renders a distinct, actionable message for each code — "Email delivery isn't wired up on this deployment yet. Set RESEND_API_KEY in Vercel…" instead of the generic retry-or-call-dispatch line — so operators wiring up a fresh deployment can read the failure off the screen instead of digging through runtime logs. Also added an opt-in AUTH_LOG_LINK_FALLBACK=1 env var that re-enables the dev-style console fallback in production (link logged to Vercel runtime logs, form shows "Check the server log"), so the operator can sign in and finish wiring Resend without being locked out of their own admin dashboard. Docs in docs/AUTH.md updated with the new env table and the failure-reason matrix.

  • Production deployments no longer fail at npm install

    Every Vercel build since PR #71 was erroring (npm error code ERESOLVE), so production stayed pinned to an old image-less build and the Wikimedia Commons photography from PR #71 never reached users. Root cause: package.json pinned three@^0.184.0 and @types/three@^0.184.1 while @google/model-viewer@4.2.0 declares a strict peer three@^0.182.0. npm 10 rejects the conflict by default. Pinned both three and @types/three back to ^0.182.0 to satisfy the model-viewer peer cleanly (three is only used as a type import in splash-screen.tsx and splash-jet.ts, so the minor downgrade has no runtime impact). Verified locally: npm install succeeds and next build completes with every fleet detail page in the prerender manifest.

  • Magic-link sign-in no longer reports false success

    Previously, when RESEND_API_KEY was missing on a deployment the /api/auth/request handler logged the link to stdout and still returned { delivered: true }, so the sign-in form proudly displayed "Check your email" while no email had been sent. In production the email helper (src/lib/auth/email.ts) now throws when the API key is absent, surfacing a real 502 send_failed to the user. In dev/preview the route still falls back to the console log but the response now carries provider: "console", and the sign-in form (src/app/sign-in/sign-in-form.tsx) renders a distinct "Check the server log — RESEND_API_KEY not configured" panel instead of the misleading email-sent confirmation.

  • Magic-link URL pinned to the canonical site origin

    /api/auth/request was building the verify URL from req.nextUrl.origin, which on a preview deployment would emit a link back to the *.vercel.app host — meaning the session cookie would set on the preview domain, not on triforce.flights, and the user would land on prod still signed-out. The route now reads NEXT_PUBLIC_SITE_URL (already used elsewhere via src/lib/site.ts) and falls back to the request origin only when the env var isn't set, so previews continue to work in isolation while real prod traffic always emails the canonical URL.

Added

7 entries
  • Real fleet photography from Wikimedia Commons

    Replaced the brand-aligned SVG placeholders with model-correct exterior photos for every jet — 6+ shots per aircraft (47 photos total across the 6-aircraft catalogue). All assets are CC0 / CC BY / CC BY-SA, sourced from Commons (stable URLs, no third-party CDN to rot like Unsplash did twice in PRs #50/#54). Pre-resampled to ≤1920px wide / mozjpeg q82 — total weight on disk dropped from 225 MB raw to 8.5 MB; Next Image further optimizes per device. Per-photo attribution table lives in docs/CREDITS.md. The placeholder SVGs and the dangerouslyAllowSVG/CSP flags they required in next.config.ts are gone.

  • Live visitor map on admin overview

    New Live Visitors card on /admin renders a Leaflet world map with a pulsing gold dot for each active site visitor and a muted dot for recent ones. Leaflet (1.9.4) and the CartoDB Dark Matter tile layer load lazily from unpkg/cartocdn on the client only — zero impact on bundle size or SSR. Custom map attribution shows Powered by Triforce alongside the required OpenStreetMap and CARTO credits. Mock data lives in src/lib/admin-data.ts (VISITORS); production swap is a 10-second poll of /api/admin/visitors backed by edge analytics → Postgres. Honors prefers-reduced-motion; map theme tuned to the admin dark palette via scoped <style>.

  • /ios App Store screenshot gallery

    New page at src/app/ios/page.tsx that renders the twelve marketing screenshots required for an App Store submission — six iPhone 6.9" shots at the exact 1290×2796 aspect ratio and six iPad 13" shots at 2048×2732. Each shot is a self-contained marketing frame (brand-tinted backdrop, bold headline, device frame with a mocked-up app scene) covering the product's headline features: mission control, jet request flow, fleet grid, ICU air-ambulance vitals, live mission tracking, on-call care team (iPhone) and ops dashboard, fleet, mission timeline, aircraft detail with AR, records, and concierge (iPad). Aspect ratios are locked via aspect-ratio CSS so any zoom-level screenshot still passes App Store Connect's upload checks. Linked off /docs and reachable directly from /ios.

  • docs/CREDITS.md + pre-wired CC-BY 4.0 model picks for every jet

    Catalogue-scanned six commercially-usable 3D models (CC BY 4.0, attribution-only, no NC restriction) across Sketchfab and Poly Pizza — Gulfstream G650ER by Luca Martina, Bombardier Global 7500 by Ahmed Mahdi, Cessna Citation X by SnowCrow (closest free silhouette to the Longitude), Pilatus PC-12/47 by helijah, Embraer Phenom 300 full-interior by Mixmamo.studio, and the Poly-by-Google generic helicopter (stand-in for the H145 until a licensed match lands). Each aircraft's model3d.credit is pre-filled in src/lib/aircraft.ts so the moment the GLB file gets dropped at public/models/aircraft/<slug>/model.glb the viewer renders the attribution under the canvas automatically — no second edit pass. The Jet3DPreview credit footer now gates on glbStatus === "ready" so the credit doesn't render under the empty-state placeholder. docs/CREDITS.md documents per-jet download / optimize / USDZ workflow, marks the two silhouette substitutes (Longitude, H145) as DD-prep replacement targets, and pre-fills the cited-asset bill of materials.

  • Per-aircraft 3D + Apple AR previews on every fleet detail page

    Both /private-jets/fleet/<slug> and /air-ambulance/fleet/<slug> now render a Jet3DPreview section (src/components/aircraft/jet-3d-preview.tsx) between the hero gallery and the spec tabs. It uses Google's <model-viewer> web component to drive desktop orbit, Android Scene Viewer, WebXR room- scale AR, and Apple AR Quick Look — the last of which requires a .usdz file alongside the .glb (Apple's Quick Look will not accept GLB). The viewer HEAD-checks /models/aircraft/<slug>/model.glb on mount: if the asset is on disk, it shows a poster + "Load 3D model" CTA (opt-in so mobile users on cellular aren't charged 5–30 MB unbidden); if it isn't, it shows a polished "Photoreal preview en route" empty state with drop-in instructions. An "Apple AR ready" pill appears in the header only when the USDZ counterpart is also on disk. Drag-to-orbit, pinch-to-zoom, auto-rotate (suppressed under prefers-reduced-motion), real-world-scale 1:1 placement, and lazy custom-element registration so the ~90 KB model-viewer module only loads on detail pages with an actual asset. Aircraft entries gained an optional model3d field ({ glb?, usdz?, poster?, credit? }) and a resolveModel3d() helper in src/lib/aircraft.ts that falls back to convention-based paths so adding a new jet's assets is purely a file-drop operation — no code changes required.

  • docs/MODELS_AR.md + rewritten public/models/README.md

    covering the GLB → USDZ pipeline with Apple's Reality Converter, the commercial-licensing constraints (CC-BY-NC is off-limits given the DD prep), per-jet size budgets, sourcing tables (CGTrader / TurboSquid / Hum3D / Sketchfab CC0 / manufacturer assets), and the public/models/aircraft/<slug>/{model.glb, model.usdz, poster.webp} drop-in convention.

  • @google/model-viewer dependency

    Installed with --legacy-peer-deps because model-viewer's peer range pins three@^0.182 while the splash screen runs three@^0.184; the three.js API surface model-viewer uses (GLTFLoader + WebGLRenderer primitives) is unchanged across those two minors, so the override is safe.

Fixed

2 entries
  • Desktop navbar moved up

    Bumped the lg+ sticky top from 1.25rem (20px) back down to 0.5rem (8px) so the floating pill rides closer to the viewport edge instead of sinking into the hero imagery on wide layouts. Mobile (0.5rem) and tablet (0.75rem) offsets are unchanged.

  • Top navbar no longer visually intersects the hero on desktop

    PR #57 pulled the lg+ sticky top flush to the viewport edge at scrollY === 0 on the theory that the 12px floating offset read as dead space without chrome. In practice the bare wordmark + nav links ended up sitting directly on the hero photo with zero separation, so on desktop the bar visually merged into the imagery instead of floating above it. Restored the floating offset and bumped lg+ from 0.75rem to 1.25rem so the pill clears the hero with more breathing room than sm/md. Removed the now-unnecessary transition-[top] since the offset no longer changes between scroll states. docs/NAVBAR.md updated.

Changed

3 entries
  • Hero overlay now adds a heavy white veil so headlines pop

    Every image-backed hero (home page + every PageHero consumer: /about, /missions, /care-team, /private-jets, /air-ambulance, /services, /contact, fleet pages, etc.) now layers a strong white wash on top of the parallax image. Light mode uses a near-opaque 92%→86% white veil so the dark headline reads crisply on top of the dark cockpit imagery; dark mode combines a moderated black gradient (55%→30%) with a 35%→22% white veil so the white headline lifts off the wash with real contrast. Single source of truth is the .hero-overlay utility in globals.css.

  • /docs page redesign

    The documentation page was rendering the entire CHANGELOG.md as a raw <pre> block — markdown asterisks, hashes, and bullet dashes all bleeding through as literal text, with no hero, no glass treatment, and no visual hierarchy between releases. Replaced the bare header with the standard PageHero (cockpit-at-dusk parallax, eyebrow + accent-tail title), promoted the three meta tiles to liquid-glass cards, rewrote "How versioning works" as an iconified two-column block, and added a dedicated changelog renderer (src/components/triforce/changelog-view.tsx) that parses the Keep-a-Changelog markdown into typed releases and draws each one as a liquid-glass article — sticky version index on desktop, per-category icon + tone (Added=emerald plus, Fixed=sky wrench, Changed=accent sparkle, Removed=rose trash, etc.), bolded item titles, and a small inline-markdown pass that handles **bold**, ` code , links`, and the HTML entities we use. No new dependencies — the parser is dependency-free so we keep the bundle lean.

  • Top navbar pulls flush to the top edge on desktop at scrollY === 0

    When the islanded chrome is invisible (the bar is just floating wordmark + nav links over the hero photo at the top of every page), the ~12px floating offset that exists for the iOS-26 pill effect just looks like dead space on a wide-screen layout. SiteHeader now overrides top to max(0px, env(safe-area-inset-top)) at lg+ while !islanded, and animates the position with transition-[top] duration-500 ease-out (motion-reduce respected) so the bar smoothly drops back into its floating position the moment the user scrolls or opens the mobile drawer. Mobile and tablet keep the existing 0.5rem / 0.75rem offsets so the pill chrome on those tiers still reads as detached. Documented in docs/NAVBAR.md.

Added

7 entries
  • Light-mode variant of the Triforce mark + favicon

    The brand mark is a 3D ruby triangle whose shading ramps from pinkish-white highlights (#ff8181/#ff5b5b) down to near-black shadow stops (#0a0000, #0e0101, #220202). That ramp reads beautifully on the dark default theme but on prefers-color-scheme: light the bottom of the mark became a heavy black wedge against a white surface — both inline (in SiteHeader/SiteFooter/wordmark) and in the browser-tab favicon. Each affected SVG (/src/app/icon.svg, /public/triforce-mark.svg, and the inline TriforceMark React component in src/components/triforce/logo.tsx) now embeds a <style> block with @media (prefers-color-scheme: light) overrides that re-tone every gradient stop and the apex glint into a saturated cinnabar/wine-red palette — preserving the 3D shading but staying within the brand red family so the mark sits on light surfaces as a polished ruby emblem rather than a dark monolith. Modern browsers (Chrome 105+, Safari 15+, Firefox) honour <style> inside SVG favicons referenced via <link rel="icon">, so the browser-tab favicon retunes too. The inline component scopes its CSS classes to the component instance via the existing useId() so multiple marks on a page (nav + footer) don't bleed styles into each other. The PWA launcher tiles (/public/icons/icon.svg, icon-maskable.svg) intentionally stay dark-tile-with-red-mark — app-launcher icons conventionally don't swap with system theme.

  • Splash-screen jet tracks the mouse

    The procedural F-22 on the load screen used to spin on a fixed sinusoidal loop. It now listens for pointermove on the window, projects the cursor to a virtual 3D target ahead of the jet, and slerps the nose toward that target with a banking roll into horizontal turns. Touch devices (no pointer events) keep the original accelerating spin. Honors prefers-reduced-motion and exits cleanly into the existing fly-away animation. Lives in src/components/triforce/splash-screen.tsx.

  • /services page — the missing aftermarket / advisory practice

    Restores the six service lines the original triforceaero.com site advertised but the rewrite dropped: pre-buy inspections, maintenance & interior revitalization, spare-parts logistics (with bonded warehousing in GVA / DXB / TEB), brokerage, fleet optimization, and management & crew training. Also brings back the concierge desk language ("one desk, one name") and an explicit independence note (no MRO/broker referral fees). New shared link in the desktop nav, mobile drawer, footer "Services" column, /more, and sitemap (priority 0.85).

  • About page now opens at 2003, not 2014

    Added a "Founded as a consultancy" 2003 timeline entry calling out the founding partners' Bombardier program-management lineage, retitled the section to "Twenty-three years from one desk to a six-continent floor," and amended the hero subtitle to lead with the consultancy origin. Brings back ~11 years of pedigree that was being thrown away in the rewrite — material for buyer's-DD narrative.

  • "Honour · Dignity · Respect · Trust" creed restored

    The four-word ribbon that hung over the original Triforce Aéronautique masthead is now a band on both /about (just below the stats) and /services (just below the hero), with a short explainer tying it to 2003.

  • Homepage "Beyond charter & medevac" band

    New section above the stacking-cards finale pointing to /services, framed as "Inspections · Maintenance · Parts · Brokerage · Training · Advisory — since 2003." Uses the same liquid-gradient card treatment as the other home-page CTAs.

  • Claude SessionStart "live brain" hook

    Every Claude Code session now opens with a freshly-computed repo snapshot — current branch and HEAD, package.json version, uncommitted changes, last 10 commits, open PRs (via gh if installed), and the top of the ## [Unreleased] block from this file. The same snapshot is written to docs/CONTEXT.md (gitignored, regenerated per session) so the agent always boots oriented instead of having to grep around. Hook lives at .claude/hooks/session-start.sh, runs synchronously in ~0.5 s, is fully offline (no network calls), and degrades silently if gh is missing.

Fixed

7 entries
  • Light-mode hero treatment — theme-aware overlay + bottom blur ramp

    The hero image always had a dark gradient overlay (from-black/65 via-black/40 to-[var(--color-bg)]) — fine in dark mode, but in light mode it dropped a black wash on a light page and then the <h1> (which inherits --color-fg, dark in light mode) went black-on-black and disappeared. The earlier band-aid (text-white forced everywhere on the hero, plus an islanded-aware white-on-hero treatment in the navbar) is now reverted in favour of a genuinely theme-aware fix: a new .hero-overlay utility keeps the original black gradient in dark mode but flips to a color-mix(in oklab, --color-bg, transparent) veil in light mode, washing the parallax photo toward the page bg so dark text on top reads. Paired with a new .progressive-blur-bottom utility (five stacked backdrop-filter layers, mirror of the existing .progressive-blur-top) so the bottom 45% of every hero dissolves into the page through a smooth blur ramp instead of a hard gradient seam. Applied to PageHero (/air-ambulance, /private-jets, /missions, /about, /care-team) and the home-page hero. Title, subtitle, back-link, navbar links and hero logo are all back on the standard semantic tokens (--color-fg, --color-fg-muted) and read in both modes / both scroll states. The tone="onDark" prop on TriforceWordmark and TriforceLogoStacked is still in the component API (used by the design-system social-post mock-ups, which intentionally render dark-on-dark for every viewer).

  • Design-system "Social" post templates render light-grey-on-white in light mode

    Four of the social-card mock-ups (InstagramSquareAmbulance, InstagramSquareCharter, EmergencyCTACard, MissionStatCard) declared their backgrounds via a Tailwind arbitrary value of the form bg-[radial-gradient(...),radial-gradient(...),#07090c] — i.e. a multi-layer background that ends in a bare hex colour. Tailwind v4's arbitrary-value parser mis-tokenises that shape, so the background-image rule never actually emitted; the cards fell through to the page bg, which is dark in dark mode (the bug stayed hidden) but #f5f6f8 in light mode — leaving every text-white glyph inside the mock-up invisible. The other social cards (InstagramStory, FacebookPost, FacebookLinkShare, TwitterPost, LinkedInPost, QuoteCard) end their stack with a linear-gradient(...) instead and parse fine, which is why only those four needed fixing. Moved the broken four off Tailwind and onto plain inline style={{ background: "..." }} strings (also passed tone="onDark" to the TriforceWordmark instances inside those cards so they render correctly regardless of system theme — social-export creatives must look identical for every viewer).

  • Jet marquee INP stall (~206 ms blocked main thread while dragging)

    The featured-jets marquee on the homepage was thrashing layout on every pointermove: each event wrote track.scrollLeft and then immediately read firstHalf.scrollWidth inside normalize(), forcing a synchronous layout pass. On a high-poll mouse (500–1000 Hz) the cost piled up well past one frame, and the scroll handler it triggered ran normalize() again. Two changes in src/components/triforce/jet-marquee.tsx: (1) the half-loop width is now cached and kept fresh with a ResizeObserver on the duplicated track, so normalize() is pure arithmetic — no layout read on the input path; (2) pointermove no longer writes scrollLeft directly — it stashes a pendingDragX that the existing rAF tick applies once per frame (with a flush on pointerup). The scroll handler short-circuits while a drag is active, since the tick is already normalising. Net effect: at most one layout read + one write per frame during drag, and the Vercel toolbar INP warning on .jet-marquee-track.is-dragging is resolved.

  • Aircraft hero/gallery imagery is now first-party

    Replaced every images.unsplash.com STOCK constant in src/lib/aircraft.ts with brand-aligned SVG illustrations shipped under public/images/aircraft/ (jet-sunset, jet-tarmac, jet-side, jet-cabin, jet-interior, jet-nose, midsize-jet, turboprop, helicopter). Each is a 16:10 dark-gradient scene with a clean aircraft silhouette in the brand palette, so the gallery on /private-jets/fleet/[slug] and /air-ambulance/fleet/[slug] is now immune to Unsplash deletions (twice in two days, see PR #50 and PR #54). Real photography can drop in at the same paths later. next.config.ts opted into dangerouslyAllowSVG with a strict script-src 'none'; sandbox CSP so the local SVGs flow through next/image safely. toSocialImage now falls back to the sitewide raster OG hero when given an SVG path, since OG/Twitter consumers reject SVG previews.

  • Jet detail-page gallery degrades gracefully when an image fails to load

    PR #50 repointed four removed Unsplash photo IDs but the underlying fragility remained: any future Unsplash deletion (or transient CDN error) would re-introduce a broken-image void on /private-jets/fleet/[slug], /air-ambulance/fleet/[slug], and anywhere else ZoomableImage is used. ZoomableImage now wires onError on both the next/image thumbnail and the lightbox <img> and swaps in a brand-aligned CSS placeholder (radial accent sheen + duotone Airplane icon + alt-text caption) when the upstream fetch fails. The loupe and zoom chip are suppressed in the failed state since there is no underlying photo to magnify. State is keyed by src so the component self-recovers when the gallery moves to the next slide. This is the durable second-tier fix; the first-tier fix is still to ship aircraft photos we control rather than depending on volatile stock-photo CDNs.

  • Jet detail-page gallery images now load

    Four of the nine Unsplash stock photos referenced by src/lib/aircraft.ts (jetInterior, jetNose, midsize, turboprop) had been removed upstream and were returning HTTP 404, so the corresponding gallery slides on /private-jets/fleet/[slug] (Gulfstream G650ER, Bombardier Global 7500, Citation Longitude, Pilatus PC-12 NG) rendered as broken <Image> placeholders behind the loupe overlay. Repointed each to a currently-live Unsplash photo (still on the allowlisted images.unsplash.com host).

  • News article share button now works

    The icon on /news/[slug] was a dead <button> with no onClick. Replaced it with a new ShareButton client component that uses the Web Share API on supported devices (iOS/Android/Edge), falling back to copy-to-clipboard with an "Link copied" affordance, and finally to a mailto: link if neither path is available. The same component now also powers the share affordances on the air-ambulance and private-jet aircraft detail pages, which had the same bug.

Removed

1 entry
  • Image hover-zoom on news cards

    The featured-article card, the newsroom grid cards on /news, and the related-stories cards on /news/[slug] all had a group-hover:scale-[1.03] / transition-transform duration-500 effect on the cover <Image>. Removed all three so cover photos stay static on hover; the card's border-color hover affordance (hover:border-accent/60) remains as the sole feedback.

Changed

4 entries
  • Hero top padding bumped so titles clear the islanded navbar

    The sticky iOS 26 Liquid Glass nav pill (h-14 mobile / h-16 desktop, plus safe-area + ~0.75rem offset) was crowding hero <h1>s on first paint — the title sat almost flush with the pill on mobile. Raised the top padding on both the shared PageHero (/missions, /about, /care-team, /air-ambulance, /private-jets) and the home-page hero from pt-14 sm:pt-20 md:pt-28 / pt-12 md:pt-24 to a unified pt-24 sm:pt-32 md:pt-40 so the pill has visible breathing room above the title at every breakpoint.

  • Unified hero typography + parallax across every page

    Every page- level <h1> now uses a new .font-hero utility: SF Pro Display on Apple devices (via the -apple-system/BlinkMacSystemFont aliases) and Inter Black (already loaded as a variable font) everywhere else, at weight 900 with -0.035em tracking and 0.98 line-height. Replaces the per-page mix of font-display text-4xl/5xl/6xl recipes that had drifted across /air-ambulance, /private-jets, /contact, /docs, /news, /news/[slug], the fleet pages, /more, /offline, /download, /design-system, both request flows, and the ComingSoon shell. Cormorant Garamond is still the body display font for h2/lead/quote — the change is scoped to hero titles only.

  • Damped scroll parallax extended to all image-backed heroes

    The home page's HeroParallax (lerp-toward-target rAF loop, settles to zero CPU, respects prefers-reduced-motion, pauses on tab hidden) was previously only used on /. It now ships on /air-ambulance, /private-jets, /missions, /about, and /care-team via a shared PageHero component (src/components/triforce/page-hero.tsx) that bundles the parallax image, the fade-to-bg overlay, eyebrow + accent-gradient title, optional back-link, and CTA slot — so adding a new hero is a one-component import. The previous bespoke font-display heroes on /missions, /about, /care-team were also using -mt-10/-mt-14 to overlap the next section onto the hero's bottom padding; that intersection has been removed in favour of normal positive spacing so the stat bands sit cleanly below.

  • PageHero responsive polish

    Subtitle column now widens with the viewport (max-w-md sm:max-w-xl md:max-w-2xl) on left-aligned heroes so longer copy on /missions, /about, and /care-team doesn't wrap into a pencil-thin column on tablet, while the center-aligned home hero keeps its tighter max-w-md md:max-w-lg. Hero h1 size also drops to 2.25rem on the smallest screens to give the gradient-accent line room to breathe under 360px wide.

Added

14 entries
  • Apple Store compliance + passwordless authentication

    Triforce now ships a complete sign-in, account, and legal stack and is structurally ready for App Store and Play Store review.

    • Magic-link auth (docs/AUTH.md). Self-contained, no Auth.js dependency, Web Crypto only so the same code runs in Node API routes and the Edge middleware. HMAC-SHA-256 signed tokens (15-min TTL) issued via POST /api/auth/request, exchanged at GET /api/auth/verify, sessions held in a single signed triforce_session cookie (HttpOnly, SameSite=Lax, Secure in prod, 30-day TTL). Email delivery uses Resend when RESEND_API_KEY is set; in dev the link is logged to the server console so the flow always works without configuration.
    • Founding admin: `Elijah@vfitter.com` (src/lib/auth/admins.ts). On first sign-in the role is computed from ADMIN_EMAILS and baked into the session cookie, so Edge middleware can gate /admin/* without a DB round-trip. Non-admins hitting /admin/* are redirected to /account?forbidden=1; unauthenticated users to /sign-in?next=….
    • Sign-in page at /sign-in — islanded card, validates email, inline send/sent/error states, links to Privacy and Terms.
    • Account page at /account — shows email, role pill, sign-in method, deep-link into the admin console for admins, Sign out of this device, and a guarded Delete my account flow that satisfies Apple guideline 5.1.1(v) (in-app account deletion). Deletion revokes the session cookie and notifies dispatch by email; once the Drizzle/Neon backend lands the same route will additionally purge the user record and cascade into Resend / Stripe / Twilio.
    • Privacy Policy at /privacy — full disclosures covering account data, request/mission data, PHI minimisation, vendor list (Vercel, Neon, Resend, Twilio, Stripe, R2, Mapbox, Sentry), retention, GDPR/CCPA rights, international transfers, children, and contact.
    • Terms of Service at /terms — including the &sect;14 *Apple App Store additional terms* block Apple expects (Apple-not-a-party, third-party-beneficiary, etc.).
    • Medical &amp; Emergency Disclaimer at /legal/medical-disclaimer, plus a permanent one-line disclaimer in the global site footer pointing users to call 911 / 112 first — required for any health-adjacent app.
    • Apple compliance checklist (docs/APPLE_COMPLIANCE.md) tracks each guideline (5.1.1(i), 5.1.1(iv), 5.1.1(v), 5.1.1(ix), 1.4, 4.0, 4.2, 4.7, 3.1.1, 5.1.5, 5.4, 2.5.1, 4.5.1) against where it’s satisfied in code, plus the App Store Connect &ldquo;App Privacy&rdquo; nutrition-label values and the App Review notes to paste into the submission.
    • Nav surfacing. Site footer gains an *Account &amp; Legal* column (Sign in, Account, Privacy, Terms, Medical &amp; Emergency); the desktop header and mobile drawer pick up a *Sign in* link; the mobile *More* page renders dynamic Account / Sign in / Sign out and Admin Console rows based on the active session; sitemap registers the new legal routes and robots.ts disallows /admin/*, /account, and /sign-in.
    • Env contract. New optional env vars: AUTH_SECRET (required in prod — openssl rand -base64 32), RESEND_API_KEY, AUTH_EMAIL_FROM. Without them the app still boots in dev with a one-time console warning and the magic link is printed to stdout.
  • "Tri" — scripted flight-desk chatbot (zero LLM, all funnel)

    A floating liquid-glass chat pill in the bottom-right corner of every page. Tap it to open a small chat panel that opens with a triage question ("Medical mission / Private jet / Browse the fleet / Something else") and routes every reply down a hand-authored branch toward a single conversion magnet at the leaf — phone for emergencies, the intake form for scheduled bookings, the trip-builder for charter, fleet pages for browsers, mailto for hospital credentialing and careers. Each branch carries its own vertical accent (red for ambulance, gold for charter) so the bubbles, chips, and final CTA button visibly match the magnet they're pulling toward. Free-text input is keyword-routed (no model) and lands on a fallback step that triages back into the four lanes. State persists to sessionStorage so the conversation survives page navigation, and the launcher respects the install-prompt's airspace (delayed appearance, dismissible per session). Flow data lives in src/lib/chat-flows.ts; UI in src/components/triforce/chat-bot.tsx; mounted globally from src/app/layout.tsx. See docs/CHATBOT.md for the full step graph and the keyword router.

  • Favourite-jets marquee on the homepage

    A new auto-animated row of curated headline jets (Gulfstream G650ER, Bombardier Global 7500, Citation Longitude, Phenom 300) sits between the FeatureRow and the ScrollStory. Cards are sized so multiple jets are visible at once across all breakpoints — mobile fits ~1.6 cards (60vw, capped at 300px), tablet ~2.3 (42vw), and desktop ~4 cards (280px) in the 1200px container. Internal padding and stat-strip type scale down to match, and the inter-card gap tightened from 16/24px to 12/16px. The track drifts left at ~32 px/sec via a rAF loop that writes scrollLeft directly — once the user hovers, focuses, wheels, drags, or touches it, the auto-scroll pauses and they can scroll freely; after ~2.2s of idle the drift resumes. Wheel verticals are promoted to horizontal pans for mouse users, mouse drag-to-pan is hand-rolled (with click suppression after an actual drag so cards don't navigate accidentally), and the loop is seamless because the list is rendered twice with scrollLeft wrapped into [0, halfWidth) each frame. Edge fade masks hide the wrap point, prefers-reduced-motion fully disables the auto-drift (cards stay scrollable), visibilitychange pauses while the tab is hidden, and a Liquid-Glass Pause / Play chip exposes the state on tablet and up. Each card shows hero image, type badge, name, tagline, and a three-up Range / Speed / Pax strip; tapping routes to the existing /private-jets/fleet/[slug] detail page. New file: src/components/triforce/jet-marquee.tsx. Supporting .jet-marquee-mask + .jet-marquee-track styles added to globals.css. Wired into src/app/page.tsx.

  • System-driven light mode

    The site now follows the operating system's prefers-color-scheme preference. There is intentionally no toggle — visitors on a light-themed device see the light palette, visitors on a dark-themed device see the dark palette, and the choice is transparent. Implementation lives in src/app/globals.css:

    • The dark palette stays the default in :root (preserves the existing look for everyone currently on dark).
    • A @media (prefers-color-scheme: light) block flips the semantic tokens (--color-bg, --color-bg-elev, --color-bg-card, --color-border, --color-border-strong, --color-fg, --color-fg-muted, --color-fg-subtle) and sets color-scheme: light so native form controls and scrollbars follow.
    • The brand accents (--color-aa red and --color-pj gold) stay identical in both modes so vertical identity is preserved.
    • The liquid-glass utilities (.liquid-glass, .liquid-glass-chip, .liquid-glass-chip-accent) were rewritten against a new set of glass tint variables (--glass-tint, --glass-rim-top, etc.) so they read as frosted-white glass on the light bg instead of pure white-on-dark highlights.
    • viewport.themeColor in src/app/layout.tsx now serves #f5f6f8 to light-mode devices for the iOS status bar / Android chrome colour, matching the new light --color-bg.
    • The splash screen wordmark switched from hard-coded #fff to var(--color-fg) so it reads as dark text against a light splash.
  • Sitewide SEO/social metadata pass

    Every public route now ships its own title, description, canonical, Open Graph (og:*) and Twitter Card (twitter:*) tags with a 1200×630 hero image keyed to the page — aircraft pages use the aircraft hero, news articles use the article hero (normalised to OG dimensions via the new toSocialImage helper), marketing pages use vertical-appropriate imagery. The root layout now declares metadataBase (so all relative OG image URLs resolve to absolute), full default openGraph/twitter blocks, a keywords list, robots/googleBot directives (with max-image-preview: large), and formatDetection. The home page (/) gained metadata it never had before — previously it inherited only the root layout default and had no canonical or social card. Admin, /offline, and /design-system are now robots: noindex,nofollow, and robots.txt disallows /admin/ and /offline in addition to the existing /request/ and /api/ exclusions.

  • Full build-out of /about, /care-team, and /missions

    The three pages were placeholder ComingSoon stubs since launch; they now ship as full marketing surfaces.

    • /about — A six-section narrative: cinematic hero with primary KPIs, mission statement, four operating principles (radial-glass cards with the brand's liquid-gradient accent), a six-chapter timeline from first flight (2014) through São Paulo (2026), a global-footprint panel listing all seven Alert-30 bases with pulsing status dots, a leadership grid (CEO, CMO, COO, CTO) with grayscale-to-color portrait hover, and a six-card certifications band (EURAMI, CAMTS, ARGUS Platinum, IS-BAO Stage 3, Wyvern, ISO 9001) — each claim is framed as audited rather than advertised.
    • /care-team — Crew composition (Physician, Flight Nurse, CCP, two-pilot cockpit), a ten-person roster with role chips, callsigns (MED-1, RN-2, CMD-1, etc.), base assignment, specialty, experience, mission count, and language pills. Includes the four Triforce crew standards (annual sim block, 12-hour duty cap, universal blood on board, annual external recert) and a hiring CTA with live open seats.
    • /missions — Operations-floor view. Cinematic hero with a pulsing "Live" badge, four KPI tiles (active missions, crews on Alert-30, median wheels-up, dispatch reliability), and a real mission-floor table listing six representative entries with status pills (IN-FLIGHT, BOARDING, DISPATCHED, STANDBY, COMPLETED), mission IDs, route, airframe + tail + crew, patient profile, and ETA. Below: a custom SVG world map rendering seven Alert-30 base nodes with breathing pulses and seven great-circle routes — active in solid accent gradient with marching ants, recent 48-hour routes in faded dashed white. Three featured case studies and three mission-report links pulled from lib/news.ts.
  • Sitemap refresh

    src/app/sitemap.ts now reflects the new page weights: /missions bumped to daily / 0.85, /about to 0.8, /care-team to 0.75. Added the previously missing public routes /download (0.7) and /more (0.3). Aircraft- and news-detail entries are unchanged.

  • iOS-style magnifier on jet imagery

    Every aircraft hero/gallery image on the detail pages (/private-jets/fleet/[slug] and /air-ambulance/fleet/[slug]) now supports two zoom layers. On a mouse-pointer device, hovering paints a 180px circular liquid-glass loupe that tracks the cursor and magnifies the photo at 2.4×, clamped to image bounds with a soft inner rim highlight so it reads like Apple's text-selection loupe. Tapping the image (or the new magnifier chip in the corner) opens a fullscreen lightbox with full iOS pinch-to-zoom: two-finger pinch with midpoint anchoring, scroll-wheel zoom centered on the cursor, drag-to-pan once zoomed, double-click/double-tap to toggle between fit and 2.5×, and Escape or backdrop-tap to close. Scale clamps 1×–6×; pan is clamped to the scaled bounds so the photo can't fly offscreen. Body scroll is locked while open and the lightbox respects prefers-reduced-motion via the existing keyframe gate. Implementation: new src/components/triforce/zoomable-image.tsx (one client component that owns both the loupe and the lightbox), wired into aircraft-gallery.tsx so the prev/next carousel keeps working with the new gesture surface. A fade-in keyframe was added to globals.css for the modal entry.

  • Duotone icons in the hamburger menu

    Each item in the mobile drawer now renders a Phosphor weight="duotone" glyph on the left of its label, tinted with the active vertical's --accent (red for Air Ambulance, gold for Private Jets). Glyphs: Air Ambulance → FirstAid, Private Jets → AirplaneTilt, Missions → Compass, News → Newspaper, Care Team → UsersThree, About → Info, Contact → EnvelopeSimple. Icon fades to --color-fg on hover via a group class so the row reads as one unit. The indicator on the right is preserved.

  • Textured splash-jet upgrade with optional real-model hot-swap

    The procedural fighter on the splash now reads as actual machined metal, not flat-shaded plastic. Three layered changes: 1. A PMREM-baked RoomEnvironment is set as scene.environment, so every PBR material gets a cube-mapped reflection (the silver fuselage now picks up real highlights as it spins). Renderer also flips on ACESFilmic tone mapping + sRGB output for the correct color response. 2. Two canvas-generated PBR textures (1024×512 body, 1024×1024 wing) paint brushed-silver substrates with panel-line grids, rivet fields, a red spine stripe, the TRIFORCE wordmark on the fuselage and a big red Triforce-triangle decal on each wing. 3. The silhouette gets F-22 detail: twin canted vertical tails replacing the single fin, slim intake scoops flanking the canopy, a pitot probe off the nose, four underwing pylons with Sidewinder- style missile bodies, torus nozzle rings around the afterburner glow, and twin exhausts that pulse in lockstep with the rim light.

  • Drop-in GLB model loader at /models/jet.glb

    Before the procedural build runs, the splash HEAD-checks public/models/jet.glb and, if present, loads it via GLTFLoader, recenters on the bounding- box midpoint and uniformly scales to ~2.4 world units so the existing camera framing + idle-spin + bank-away exit choreography all still work without modification. Means an operator can swap in any CC0 / CC-BY fighter (from Sketchfab, NASA, Poly Pizza, etc.) with zero code changes. See public/models/README.md for the license + size budget rules. Failure is silent — the procedural jet is the always-on fallback. Build code lives in src/components/triforce/splash-jet.ts.

  • Design system page at /design-system

    A single source of truth for every surface Triforce ships, accessible from the footer and listed in the sitemap. Sections cover Foundations (brand mark, wordmark, stacked lockup, clear-space), Color (brand + neutral tokens — Ambulance red and Charter gold variants with click-to-copy swatches), Typography (Cormorant Garamond display + Inter UI scale), Surfaces (the three Liquid Glass primitives — .liquid-glass, .liquid-glass-chip, .liquid-glass-chip-accent — plus card elevations), Components (buttons, badges, tabs, KPI tiles, inputs), Motion (.ios-reveal, .liquid-gradient, press feedback), and Voice & Tone (do/don't, bylines, timestamps).

  • Ten ready-to-export social-media post templates

    rendered from scratch with the same Liquid Glass + radial-accent vocabulary as the marketing site, so off-platform posts visibly belong to the same brand: Instagram square (Air Ambulance Mission Report, Private Jets range announcement), Instagram Story/Reel 9:16, Facebook native post with full chrome (page row, reactions, action bar) and Facebook link-share with OG card, X / Twitter post with 16:9 media, LinkedIn company-page post with KPI tile grid, and three reusable creatives (quote card, emergency CTA, mission-stat carousel slide). Each post is captioned with the export resolution it targets. See docs/DESIGN_SYSTEM.md.

  • Save-to-Contacts vCard download on /contact

    A new "Download Triforce vCard" card on the contact page links to a static /api/vcard route that emits a RFC 2426 vCard 3.0 with Content-Type: text/vcard and Content-Disposition: attachment; filename="Triforce.vcf" — the combination iOS Safari and Android Chrome need to hand the file off to the native Contacts app. The vCard embeds the dispatch hotline (TEL;TYPE=WORK,VOICE), the dispatch and press emails, the canonical https://triforce.flights URL, organization, title, and the "staffed every minute of the year" note. Contact metadata is now centralized in src/lib/contact.ts so the page UI and the vCard payload share a single source of truth and cannot drift.

Changed

1 entry
  • Fleet search bar sticks to the viewport on mobile

    On /private-jets/fleet and /air-ambulance/fleet, the search input + filter button row in FleetExplorer (src/components/triforce/fleet-explorer.tsx) now uses position: sticky below the islanded navbar (top-[calc(env(safe-area-inset-top)+4.25rem)], z-30) with a translucent glass backdrop (bg-[color:var(--color-bg)]/85 backdrop-blur) and full-bleed negative margin (-mx-5 px-5) so the chrome reads continuously as the user scrolls long fleet lists on a phone. The behavior is mobile-only — md: and above revert to the inline, non-sticky layout (md:static md:bg-transparent md:backdrop-blur-0 md:mx-0 md:px-0) since desktop already shows the whole fleet without much scroll. The category filter chips below remain non-sticky to keep the pinned area as compact as possible.

Fixed

3 entries
  • Aircraft detail hero clears the iOS PWA navbar in full, not just by a few pixels

    The previous fix added a flat mt-3 md:mt-4 (12/16px) to AircraftGallery, but the stuck SiteHeader pill on an iPhone PWA actually occupies env(safe-area-inset-top) + pt-1 + h-14 ≈ 107–119px from the viewport top (≈80px on desktop), so the hero image was still being covered by the navbar on /private-jets/fleet/[slug] (and, by the same component, the air-ambulance detail page). Replaced the static margin with mt-[calc(env(safe-area-inset-top)+5rem)] md:mt-[calc(env(safe-area-inset-top)+5.5rem)], which dynamically tracks the iOS safe area on top of a full pill-height clearance — so the gallery starts cleanly below the navbar on every device, with a small visual buffer. Also re-anchored the air-ambulance detail page's mobile back/save/share button row from top-20 to top-[calc(env(safe-area-inset-top)+6rem)] so it stays inside the gallery hero instead of floating above it.

  • Mobile bottom nav no longer clips behind the iOS home indicator

    BottomNav (src/components/triforce/bottom-nav.tsx) was pinned with sticky bottom-0 but had no env(safe-area-inset-bottom) padding, so on iPhone PWAs / Safari the labels and icons sat under the home indicator and read as half-cut. Added pb-[env(safe-area-inset-bottom)] to the nav — viewport is already viewportFit: "cover" in src/app/layout.tsx, and this matches the safe-area pattern already used by install-prompt.tsx and the splash screen rules in globals.css.

  • Public contact emails now use the production domain

    The contact page, news index, and per-article media-inquiries block were all publishing dispatch@triforce.example / press@triforce.example.example is RFC 2606 reserved and would never deliver mail. All public-facing copy now matches the canonical address already used by src/app/admin/settings/page.tsx and the staff records in src/lib/admin-data.ts: dispatch@triforce.flights and press@triforce.flights. The fallback in src/lib/site.ts (NEXT_PUBLIC_SITE_URL default) was bumped from https://triforce.example to https://triforce.flights for the same reason — production deploys override via env var, but the fallback now resolves rather than 404s.

Added

4 entries
  • Progressive blur band at the top of the viewport

    A new <TopBlurOverlay> is mounted once in the root layout and renders a fixed band across the top edge of every page. The band is five stacked backdrop-filter layers (blur 0.5 → 1.5 → 4 → 10 → 20 px, the last with saturate(140%)), each masked with a cascading linear-gradient so the heaviest blur sits flush with the viewport's top edge and softens to crisp by the band's bottom. The result is a smooth blur ramp — content fades into the islanded navbar pill instead of slipping behind a hard backdrop-blur cut. The band height tracks env(safe-area-inset-top) plus 96 px on mobile / 128 px on ≥md, sits at z-30 (below the sticky <SiteHeader> at z-40, above page content), and is pointer-events: none so it never traps clicks. Honors prefers-reduced-motion by collapsing to a single 6 px blur with a simple top-to-transparent mask. Styles live as the .progressive-blur-top utility in globals.css.

  • Lazy-loaded splash screen with a Three.js spinning jet

    First paint is now a fixed-position black veil with a slowly rotating accent halo, the Triforce wordmark, a thin red progress bar, and a stylized white-and-red jet (capsule fuselage, swept extruded wings, red wing-tip strips, emissive engine glows, dark canopy) banking inside two concentric red rings. Three.js is dynamic-imported inside the SplashScreen component so the ~600 KB three bundle ships in its own chunk and never blocks the initial paint — until it arrives, a CSS-only spinning Triforce SVG fills the canvas slot. The splash holds for a minimum 1.9 s (350 ms in reduced-motion), waits for window.load, and has a 6 s safety net so it can never trap the user. On hand-off the jet hard-banks and flies out of frame as the splash fades and blurs away. Honors prefers-reduced-motion (no canvas, snap fade), pauses on document.visibilitychange (per CLAUDE.md), disposes every geometry, material, and renderer on unmount, and falls back gracefully via <noscript> so JS-disabled visitors aren't trapped behind it. See docs/SPLASH.md.

  • Hypnotic cascading lazy-unblur of the page below the splash

    While <html data-splash="loading"> is set (server-rendered to avoid FOUC), the new .splash-content wrapper around {children} is hidden under a 28 px blur with pointer-events: none and body { overflow: hidden }. When the splash flips the attribute to data-splash="ready" every direct child of .splash-content (header, main, footer, bottom nav) and every <section> inside <main> transitions from blur 18 px → crisp on a staircase of transition-delays (60 → 980 ms), so the reader's eye is led top-to-bottom through the page rather than being flooded with everything at once. After the exit animation the splash unmounts, data-splash="done", and subsequent client-side navigations bypass the splash entirely.

  • Navbar buttons (hamburger + 24/7 Emergency CTA) now use a new .liquid-glass-chip / .liquid-glass-chip-accent utility so they read as glass-on-glass against the islanded liquid-glass pill — top-left radial highlight, inset rim, light backdrop blur. The accent variant tints the glass with var(--accent) so the hue still tracks the active vertical (red for ambulance, gold for charter).

Changed

2 entries
  • Navbar buttons (hamburger + 24/7 Emergency CTA) swapped from rounded-full pills to concentric rounded-xl md:rounded-2xl (12px / 16px) so their corner radii follow the parent navbar's rounded-2xl md:rounded-[28px] (16px / 28px) family. With the ~10px vertical inset between the button and pill edges, this matches the iOS Liquid Glass "concentric rounding" rule (inner = outer − inset) and the buttons now nest cleanly inside the pill instead of floating as standalone circles.

  • Global interactive-feedback CSS so every clickable element (buttons, anchors, [role="button"], summary, form button inputs) gets a subtle hover lift (+1px / scale 1.015) and a snappy pressed-in state (scale 0.96). Easing is springy ease-out on release (cubic-bezier(0.34, 1.56, 0.64, 1), 260ms) and a fast ease-in on press (90ms) so taps feel poppy and clicky. Respects prefers-reduced-motion; per-element opt-out via .no-press.

Changed

6 entries
  • Home hero is now tighter on phones: the headline drops from text-5xl (48px) to text-[2.25rem] (36px) below the sm breakpoint and the sub-copy steps down from text-lg to text-base, so "Anytime. Anywhere." no longer crowds the viewport on ~390px devices. Tablet and desktop sizing (sm:text-5xl md:text-7xl) are unchanged.

  • Navbar wordmark: enlarged the small Triforce mark from h-7 w-7 to h-9 w-9 so the icon visually spans both lines of text, and tightened the gap between "TRIFORCE" and the "AIR AMBULANCE" / "PRIVATE JETS" tagline from mt-1 to mt-0.5 for a denser, more balanced lockup.

  • Replaced the placeholder Triforce mark with a 3D metallic-red Penrose-style triangle (faceted gradient, inner sub-triangle, outer stroke). Mark uses per-instance useId gradient ids so multiple logos can render on the same page without collisions.

  • SiteHeader now centers the wordmark in a 3-column grid: hamburger / left nav links on the left, centered TriforceWordmark, right nav links + emergency CTA on the right. Desktop nav links split 2/3 around the logo (Air Ambulance + Private Jets on the left; Missions + News + About on the right).

  • Home hero swapped the inline horizontal wordmark for a new TriforceLogoStacked component (mark above, "TRIFORCE" wordmark, "AIR AMBULANCE" tagline) and centered the headline + CTAs beneath it for a brand-forward landing. Composed with the existing ios-reveal-* entrance animations.

  • Top navbar now only "islands up" once the page has scrolled. At scrollY === 0 the pill is naked (no border, shadow, blur, or accent sheen) and the wordmark / nav links sit transparently over the hero. As soon as the user scrolls, the liquid-glass chrome fades in over 500ms (opacity-only, GPU-cheap) so the bar appears to crystallize into the iOS 26 island. Mobile drawer open also forces the islanded state so the chrome doesn't visually disconnect from the open menu. prefers-reduced-motion skips the transition.

Removed

1 entry
  • Top-navbar WebGL metaball orbs (NavbarOrbs) and the three / @types/three dependencies they were the sole consumer of. The accent conic-gradient sheen and liquid-glass surface remain; the bluish orbs that bled through the blur are gone.

Fixed

4 entries
  • Sitemap no longer lists /request/air-ambulance or /request/charter. robots.txt already disallows the /request/ prefix, so including those URLs in the sitemap sent conflicting crawl signals (flagged on PR #11).

  • Restored the lucide-react dependency. The admin console (PR #4) imports ArrowUpRight/ArrowDownRight from lucide-react, but PR #5 had removed the package — leaving main unable to build. Re-adding it unblocks the deploy. Follow-up: convert the admin surface to Phosphor duotone icons to match the marketing site, then remove lucide-react again.

  • Top navbar now respects env(safe-area-inset-top) so when the site is installed as a PWA on iOS (Add to Home Screen) the pill clears the Dynamic Island / notch instead of being clipped behind it. Implemented with a margin-top that pre-positions the header just below the island plus a matching sticky top, so the pill stays at the same vertical position whether the page is at scroll 0 or scrolled. Previous iteration double-counted the inset on padding *and* sticky-top, which caused the pill to drop ~60px once the user started scrolling on iPhone.

  • Mobile bottom-nav labels no longer wrap to two lines on narrow phones. Removed the uppercase + 0.16em letter-spacing that was widening every cell, shortened "Care Team" to "Care" (single-word labels match standard iOS/Material tab-bar convention), and added whitespace-nowrap as a safety net.

Added

21 entries
  • /icon.svg (Next.js App Router favicon) and /public/triforce-mark.svg using the same Penrose mark, wired up via metadata.icons in app/layout.tsx. Old favicon.ico removed.

  • TriforceLogoStacked size variants (md / lg / xl) for centered hero/marketing use. The same Penrose mark also replaces the placeholder public/icons/icon.svg + icon-maskable.svg used by the PWA manifest, so the installed home-screen icon matches the brand.

  • PWA install support

    Triforce is now installable on iPhone/iPad (Safari → Share → Add to Home Screen), Android (Chrome beforeinstallprompt), macOS, and Windows. Manifest (/manifest.webmanifest) declares display=standalone, themed icons (SVG, with maskable variant), three home-screen shortcuts (Air Ambulance, Charter, Hotline), and the theme_color matches the dark shell so the system chrome blends seamlessly.

  • Service worker

    (/sw.js, registered in production only) with network-first HTML, cache-first static assets, and an offline fallback page at /offline so the installed app still loads when the user is out of signal.

  • Smooth Liquid-Glass install banner

    (InstallPrompt) that appears after a 4.5s delay, auto-picks the right flow per platform: iOS Safari shows a Share-button hint with a navigator.share() shortcut + caret pointing at the share affordance; Android/desktop Chromium gets a one-tap "Install app" backed by the captured beforeinstallprompt event. Includes a "Don't show this again" action that writes triforce.installPrompt.dismissed=forever to localStorage, and a soft dismiss that snoozes for 7 days.

  • /download page

    with auto-detected device card highlighted, full install instructions for iPhone/iPad, Android, macOS and Windows, and a Hybrid Roadmap card documenting upcoming App Store (Capacitor) / Play Store (TWA) / DMG / MSIX builds.

  • Footer + /more page now link to Download App.

  • Newsroom: /news index and statically-generated /news/[slug] detail pages with three launch articles (press release, mission report, fleet update). Each article ships with NewsArticle JSON-LD, OpenGraph/Twitter metadata, and a structured block renderer (lead, paragraph, heading, quote, list). News is linked from the desktop nav, mobile drawer, footer, and /more. The new articles are also surfaced in public/llms.txt and the XML sitemap.

  • app/sitemap.ts (Next.js Metadata Routes) generating an XML sitemap at /sitemap.xml for all marketing pages, news articles, and fleet detail pages, with per-section changeFrequency and priority.

  • app/robots.ts generating /robots.txt that allows crawling, blocks /api/, /_next/, and the /request/ flows, and points search engines to the sitemap.

  • SITE_URL helper in src/lib/site.ts, defaulting to https://triforce.example, overridable via NEXT_PUBLIC_SITE_URL.

  • iOS-style scroll storytelling on the home page. A new ScrollStory component pins a hero visual on one side while five chapter panels scroll on the other, narrating one anonymised Triforce mission from the call (T+00:00) to touchdown and back to "Always On". Active chapter cross-fades the pinned image with blur + scale on a cubic-bezier(0.16, 1, 0.3, 1) "ease-out-quint" easing.

  • Hero parallax: background image scales + drifts + softens as the viewport scrolls past, via native animation-timeline: scroll(root) on Chromium with no JS cost. iOS-style scroll indicator that fades out after the first viewport.

  • StatReveal strip with rAF count-up numbers (ARGUS / IS-BAO / 2,400 missions / 187 countries) triggered by IntersectionObserver.

  • StackingCards finale: three sticky-top cards rise into a stack, iOS-Photos-app style, each linking to one of the verticals.

  • Decorative marquee divider between chapters and stats.

  • Reusable scroll-driven CSS toolkit in globals.css (.ios-reveal, .ios-reveal-up, .ios-reveal-down, .ios-chapter, .ios-bignum, .ios-stack-card, .ios-parallax-bg, .ios-pin-image, .ios-marquee, .ios-stagger) — uses native animation-timeline: view() / scroll() where supported and falls back to an IntersectionObserver-driven .is-in class via RevealOnScroll. All animations respect prefers-reduced-motion.

  • public/llms.txt so LLM crawlers can ground on the marketing site cleanly (per the llmstxt.org convention).

  • Islanded liquid-glass top navigation (SiteHeader) inspired by iOS 26 Liquid Glass. Detached floating pill, animated conic gradient sheen, and a glass mobile drawer.

  • Responsive nav tiers: full link set on desktop (≥1024px), condensed two-link set on tablet (768–1023px), and a glass drawer + emergency-call pill on mobile (<768px).

  • New .liquid-glass and .liquid-gradient utilities + liquid-spin keyframes in globals.css.

Changed

3 entries
  • Hero typography now leans on a brighter, bolder treatment so the display copy holds the dark hero photo. Title scale bumped (text-5xl → text-7xl on desktop) and weight raised to font-semibold / font-bold on the accent line. The accent spans on the home hero ("Anytime. Anywhere.") and closing line ("We do.") now use a new .text-gradient-accent utility that lifts the top of each glyph toward an accent‑white mix — the raw #e11d2e / #c8a464 accents were reading as muddy against --color-bg. Body paragraph under the hero is now font-medium at text-lg / text-xl on full --color-fg (was muted grey) so the lede no longer disappears next to the title. Cormorant Garamond weight 700 added to the font import to support the new bold display weight.

  • Hero parallax is now JS-driven with damping instead of CSS animation-timeline: scroll(). The previous version was Chrome-only and had no smoothing — every scroll tick applied directly. The new HeroParallax client component runs a requestAnimationFrame loop that lerps the displayed transform toward a scroll-derived target (damping factor 0.085), which produces the "delayed" feel and a soft spring-like settle. It works in Safari/Firefox, pauses on hidden tabs, exits the rAF loop when settled (zero CPU outside scrolling), and skips entirely under prefers-reduced-motion.

  • Top navbar now only "islands up" once the page has scrolled. At scrollY === 0 the pill is naked (no border, shadow, blur, or accent sheen) and the wordmark / nav links sit transparently over the hero. As soon as the user scrolls, the liquid-glass chrome fades in over 500ms (opacity-only, GPU-cheap) so the bar appears to crystallize into the iOS 26 island. Mobile drawer open also forces the islanded state so the chrome doesn't visually disconnect from the open menu. prefers-reduced-motion skips the transition.

Removed

1 entry
  • Top-navbar WebGL metaball orbs (NavbarOrbs) and the three / @types/three dependencies they were the sole consumer of. The accent conic-gradient sheen and liquid-glass surface remain; the bluish orbs that bled through the blur are gone.

Fixed

5 entries
  • Install banner now surfaces a macOS Safari variant pointing at File → Add to Dock…. Safari 17+ supports installable web apps, but it never fires beforeinstallprompt, so the previous variant matrix silently skipped Safari desktop users entirely (flagged by code-review bot on PR #12).

  • Sitemap no longer lists /request/air-ambulance or /request/charter. robots.txt already disallows the /request/ prefix, so including those URLs in the sitemap sent conflicting crawl signals (flagged on PR #11).

  • Restored the lucide-react dependency. The admin console (PR #4) imports ArrowUpRight/ArrowDownRight from lucide-react, but PR #5 had removed the package — leaving main unable to build. Re-adding it unblocks the deploy. Follow-up: convert the admin surface to Phosphor duotone icons to match the marketing site, then remove lucide-react again.

  • StatReveal reduced-motion early-exit no longer trips React 19's react-hooks/set-state-in-effect rule — the synchronous setValue(target) is now scheduled inside a one-shot requestAnimationFrame.

  • Mobile bottom-nav labels no longer wrap to two lines on narrow phones. Removed the uppercase + 0.16em letter-spacing that was widening every cell, shortened "Care Team" to "Care" (single-word labels match standard iOS/Material tab-bar convention), and added whitespace-nowrap as a safety net.

Released

v0.1.0

2026-05-10

Added

3 entries
  • Documentation page at /docs listing the current app version, environment info, and changelog.

  • Footer link to the documentation page.

  • Marketing site scaffolding: home, air ambulance, private jets, about, contact, missions, care team, request flow, and more.

TRIFORCEAIR AMBULANCE

World-class air ambulance and private charter — trusted by hospitals, chosen by families. Mission control around the clock, every day of the year.

Services

Account & Legal

Worldwide Hotline
00 800 TRI FORCE
874 367 23
All regional numbers →
ARGUS Platinum · IS-BAO Stage 3 · EURAMI Accredited
In a life-threatening emergency, call 911 (US), 112 (EU), or your local emergency number first. Triforce coordinates air-medical and charter transport — we are not an emergency dispatch service. Read the full disclaimer.
© 2026 Triforce. All rights reserved.v0.1.0Trusted by Hospitals. Chosen by Families.