Unreleased
Added
1 entryGCC & 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 thedocs/I18N.mdcoverage matrix.
Changed
2 entriesPERF: AVIF image format + tuned deviceSizes/imageSizes in
next.config.tsNext.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-tuneddeviceSizes(390, 640, 768, 1024, 1280, 1536, 1920) andimageSizes(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. Addedplaceholder="blur"and a sharedblurDataURL(dark#0d0d14SVG matching the site's near-black background, viasrc/lib/blur-placeholder.ts) to five components:aircraft-card.tsx— fleet list and search result cardsjet-marquee.tsx— homepage marquee tile imagesstacking-cards.tsx— homepage service verticals hero cardsscroll-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
PageHeroalready has its own warm-gradientblurDataURL(from PR #284); this PR covers every other image-bearing surface.
Fixed
5 entriesA11Y: Admin panel loading spinners now respect
prefers-reduced-motionTwo
<Loader2 className="animate-spin" />spinners in the integration config panel (Test and Save button states) were missing themotion-reduce:animate-noneTailwind variant. Users withprefers-reduced-motion: reduceset 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. Addedmotion-reduce:animate-noneto 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) withoutloadingordecodingattributes. 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. Addeddecoding="async"so the browser can decode off the main thread without holding up layout, andloading="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 dimensionsThe QR code rendered inside the aircraft detail panel (AR launch flow) was missing explicit
widthandheightattributes, even though the image is always generated at 512 × 512 px. Addedwidth={512} height={512}anddecoding="async"— aligns with the existingaircraft-picker.tsxpattern, 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-motionThe 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 RTLanimation-nameoverride in the global stylesheet sat outside theprefers-reduced-motionguard block, creating a cascade ambiguity. Added an explicithtml[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/arThe 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). Addedplaceholder="blur"with a hand-craftedblurDataURL— 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; thefillcontainer dimensions are unchanged.
Added
3 entriesAircraft for Sale —
/private-jets/for-saleand/ar/private-jets/for-saleNew 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
/contactpre-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 withuseState+useMemofor filter state. Bilingual content embedded inline (same pattern astriforce-select.tsx). Accepts alocaleprop; all copy, AFTT/seat/range labels, availability text, and category names are bilingual. Numerical values taggeddata-keep-ltrfor Arabic.src/app/private-jets/for-sale/page.tsx— English server component with full OG/Twitter metadata andbuildHreflangAlternates.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;ArrowRighticons carryrtl:rotate-180; navigation links resolve to/ar/*paths./ar/private-jets/for-saleregistered inAR_BUILT_ROUTESinsrc/middleware.ts(prevents 307 redirect to English)./private-jets/for-saleadded tosrc/app/sitemap.tswithchangeFrequency: "weekly",priority: 0.85, andlocalized: 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, arequiredflag, and anisSecretflag that swaps the input totype="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.tsx—IntegrationsSettingsextended with anonConfigurecallback; the top-levelSettingsTabscomponent holdsconfigTargetstate and renders<IntegrationConfigPanel>at the root so it overlays the full admin shell correctly.
Private Aviation Market Intelligence page —
/intelligenceand/ar/intelligenceNew 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 withuseStatefor active tab; data embedded with{ en, ar }content objects (same pattern ascertification-band.tsx). Imported vianext/dynamic({ ssr: true })in both page files.src/app/intelligence/page.tsx— Server component,vertical="charter"accent (gold), fullPageHero+ section layout.src/app/ar/intelligence/page.tsx— Arabic RTL version; all prose translated to Modern Standard Arabic; numerical values taggeddata-keep-ltr;dirpropagated to grid/flex containers.- 37 new i18n keys added to the
Dictionarytype (intelligence.*), with EN and AR values inDICTIONARIES. /ar/intelligenceregistered inAR_BUILT_ROUTES(middleware)./intelligenceadded tositemap.tswithchangeFrequency: "quarterly"andlocalized: true.
Fixed
4 entriesA11Y: Simulator HUD dialogs — focus trap + Escape-to-close
—
FailuresPanelandFidelityCardwere 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 useuseFocusTrap(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.transitionassignment, 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-subtledark-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. usetext-xswith this token). The token is updated to#808d9a, yielding ≈ 5.5 : 1 on--color-bg-elevand ≈ 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
CommandPalettecomponent (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 intocommand-palette-store.tsso thatsite-headercan import just the tiny trigger functions, while the heavy UI component is split into a separate async chunk vianext/dynamicinsideOverlayShell. 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 entryA11Y: 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 entriesMulti-currency selector —
/financingand/ar/financingAll 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
/financingand Arabic/ar/financingroutes.Architecture:
src/lib/currency.ts— exchange rates,CurrencyCodetype,makeFmt()(full formatter) andmakeFmtCompact()(SVG chart labels) factory functions backed byIntl.NumberFormat.src/components/triforce/currency-selector.tsx— five pill-tab buttons (role="group",aria-pressed), Liquid Glass styling,data-keep-ltron labels for RTL safety.src/components/triforce/financing-client.tsx— thin"use client"wrapper that owns a singlecurrencystate and passes it down to all three calculator components, keeping their state in sync.- Each calculator component now accepts a
currency?: CurrencyCodeprop;useMemomemoizes the formatter factory on currency change; allIntl.NumberFormatcalls replaced withfmt(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 —
/financingand/ar/financingNew 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
ResidualValueChartselector. - 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.tsxPROFILES (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 theDictionarytype insrc/lib/i18n.ts— full English and Arabic (MSA) translations. Missing key is a TypeScript compile error. - Both financing pages updated:
src/app/financing/page.tsxandsrc/app/ar/financing/page.tsx. - RTL-safe layout: numbers tagged
data-keep-ltr,text-start/text-endfor column alignment,ms-*logical spacing. Arrow icon mirrored in RTL. - Design:
.liquid-glasssurface,.ios-reveal/.ios-reveal-upscroll animations,var(--accent)accent colour (tracks charter-gold vertical),.liquid-glass-chip/.liquid-glass-chip-accenttab pills — consistent withResidualValueChartandOwnershipCalculatordirectly above it on the page. - Responsive: single-column on mobile (table → scrollable), two-column lg:grid on desktop (matrix left, stats sidebar right).
- 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
Fixed
1 entryA11Y — 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); onsm+ the text label reappears alongside the icon (sm:px-4,hidden sm:inlineon the label span).aria-labelandaria-pressedare 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-ltron 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 assrc/lib/admin-data.ts— Drizzle-ready for when the backend ships.src/components/portal/portal-shell.tsx— client component (usePathname for active nav state). Acceptslocale: Locale+labels: PortalLabelsprops (server page callsgetTranslator, 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,VerticalChipfromsrc/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/portaladded toAR_BUILT_ROUTES;/ar/portal/missions,/ar/portal/documents,/ar/portal/invoicesadded toAR_BUILT_PREFIXES(covers the[id]subtree for mission detail).- 67 new
portal.*keys added to theDictionarytype insrc/lib/i18n.tswith 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 entryA11Y: Global keyboard focus ring — WCAG 2.4.7 + 2.4.11 compliance
Added a site-wide
:focus-visiblerule toglobals.cssthat 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-visibleselector has specificity0-1-0. - Components that already define their own ring (Button, Tab, Accordion, Modal close, Toast dismiss, Command palette) all carry
focus-visible:outline-noneTailwind class (specificity0-2-0), which wins and suppresses the global outline — no double-ring. - Form inputs use
.form-input:focus { outline: none }(specificity0-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: noneinside:focus— same win. - Previously undecorated elements (nav links, glass chips) now receive the outline.
Design consistency:
--accenttracks 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 userounded-xl) modern browsers draw the outline following theborder-radius, keeping the ring pill-shaped.- The
Added
4 entriesClient Success Stories —
/success-storiesand/ar/success-storiesNew 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.tsxandsrc/app/ar/success-stories/page.tsx. /ar/success-storiesadded toAR_BUILT_ROUTESinsrc/middleware.ts./success-storiesadded toSTATIC_PATHSinsrc/app/sitemap.tswithlocalized: true— Google will index both language versions with correct hreflang alternates.- Design uses existing
liquid-glass,liquid-gradient,ios-reveal,ios-reveal-uputilities;PageHero,RevealOnScroll,SiteHeader,SiteFooter,BottomNavfrom the existing component library. - Fully RTL-safe: logical Tailwind utilities (
end-*,start-*) throughout;data-keep-ltron 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.languagesset viabuildHreflangAlternates("/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/Minusiconography 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— takeslocale: Locale, renders bilingual content viaisArflag. 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) taggeddata-keep-ltr. Layout uses logical Tailwind utilities throughout (ms-*,me-*,ps-*,pe-*,start-*,end-*), withdirapplied at section level. - Both pages use the
private-jetsvertical layout (gold/champagne accent, charter header). /ar/private-jets/selectadded toAR_BUILT_ROUTESinsrc/middleware.ts./private-jets/selectadded toSTATIC_PATHSinsrc/app/sitemap.tswithlocalized: 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 —
/imprintand/ar/imprint(src/app/imprint/page.tsx,src/app/ar/imprint/page.tsx)Added a dedicated Imprint page at the canonical
/imprintpath (and/ar/imprintin 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/imprintadded toAR_BUILT_ROUTESinsrc/middleware.ts(prevents the Arabic untranslated-route 307 fallback)./imprintadded toSTATIC_PATHSinsrc/app/sitemap.tswithlocalized: true— Google will discover and index both language versions."footer.imprint"key added to the i18nDictionarytype insrc/lib/i18n.tswith 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
SelectUI 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 theliquid-glasssurface andform-inputstyling established by the rest of the product.The new
Selectcomponent implements the WAI-ARIA 1.2 Select-Only Combobox pattern:- Trigger:
<button type="button" role="combobox">styled with theform-inputbase class (16 px font size — iOS Safari zoom prevention per CLAUDE.md), liquid-glass focus ring mirroringform-input:focusexactly. - Dropdown: portal-rendered
<ul role="listbox">withliquid-glasssurface,liquid-gradientcorner accent sheen, andbackdrop-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 theendside of each option row. - Form submission: optional
nameprop 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-hiddenclipping 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-systemhas 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.
- Trigger:
Changed
1 entryPress 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-3card structure handles the marginally taller text block viaitems-stretchin the marquee. Nomin-hor 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.
- Quote text:
Fixed
3 entriesCardRail arrow buttons keyboard-accessible on tablet (
src/components/ui/card-rail.tsx)The Prev/Next navigation arrows on the
CardRailcarousel were simultaneously markedaria-hiddenand giventabIndex={-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-hiddenhides the element from the accessibility tree — yet each button also had anaria-label. A labelled element that is simultaneouslyaria-hiddencontributes 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-hiddenfrom both buttons. At mobile and desktop, the buttons aredisplay: none(via Tailwind'shidden md:flex lg:hidden), which already removes them from both the tab order and the accessibility tree —aria-hiddenwas redundant and harmful. At tablet they are now properly exposed. - Removed
tabIndex={-1}from both buttons. Natural tab order applies; at mobile/desktopdisplay: nonekeeps them off the tab stop list. - Replaced
pointer-events-none opacity-0at-edge styling with the HTMLdisabledattribute.disabledremoves the button from the tab order and marks it as unavailable to AT — correct semantics for a control that cannot be activated. The existingopacity-0visual style is retained so the design is unchanged. - The scroll-track keyboard mechanism (
onKeyDownArrow-key handler on therole="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_configuredpath):"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_configuredpath):"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.
- Charter form (
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-nonewithout afocus-visible:ringfallback, 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, keepingfocus:outline-noneto 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-40on 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 existingfocus: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.- Standalone inputs (command palette, chat, admin search): added
Added
4 entriesArabic request pages —
/ar/request/charterand/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/charterand/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, andBottomNavall render withlocale="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-ltron theEURAMIcredential name. - RTL-safe back navigation:
CaretLefticon usesrtl:rotate-180to mirror correctly as a rightward "back" caret in the RTL reading direction. - Proper SEO:
Metadata.alternates.languageswired viabuildHreflangAlternates("/request/charter")andbuildHreflangAlternates("/request/air-ambulance");og:localeset toar_SA; both Arabic URLs added to the XML sitemap (localized: true) at priority 0.85 and 0.9. - Both routes registered in
AR_BUILT_ROUTESinsrc/middleware.ts— no longer fall through to the English redirect. docs/I18N.mdcoverage matrix updated; follow-up note added documenting thatTripBuilder/AirAmbulanceIntakeform-field labels remain English (structured input fields — airport codes, date pickers, cabin selectors — are treated as internationally legibledata-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.- Full Arabic page chrome:
Charter Services section on
/private-jetsand/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-ltron Latin model names,ShieldCheckdetail chip mirrored byend-*positioning). No new i18n.ts keys needed — content is colocated in the component constants following themedical-capabilities.tsxandoperational-band.tsxpattern.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-fadeanimations 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.tsrendered 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 existingRouteCity.nameArandpopularForArfields — no new data needed. AED/USD pricing left in USD throughout for consistency with the rest of the site.Infrastructure:
/ar/destinationsadded toAR_BUILT_ROUTESin middleware so the route resolves instead of 307-redirecting to English;/destinationsadded toSTATIC_PATHSinsrc/app/sitemap.tswithlocalized: true(emits both English and Arabic sitemap entries with hreflang alternates). Metadata includesMetadata.alternates.languagesviabuildHreflangAlternates("/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-srcinnext.config.tsupdated to includehttps://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 inTripBuilderon successful/api/request/charterresponse.ambulance_request_submitted(urgency: 'emergency' | 'standard') — fires inAirAmbulanceIntakeon successful/api/request/air-ambulanceresponse.
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 entryA11Y:
<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, orBottomNav. 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 alandmark-one-mainWCAG 2.4.1 Level A violation.Fix: added
SiteHeader vertical="charter",<main id="main-content" className="flex-1">wrapper,SiteFooter, andBottomNavto all four pages (withlocale="ar"on the Arabic variants). Also addedid="main-content"to the<main>inAdminShellso 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 entryHero 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-upscroll-driven path resolves above-fold elements toopacity: 1at 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 sameios-rise/ios-rise-noblurkeyframes as the scroll-driven toolkit but as time-based animations, matching the logo-assembly pattern already used by.logo-stacked-reveal. A--reveal-delayCSS custom property on each element drives the stagger.prefers-reduced-motion: reducedisables 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 entriesComparisonMatrixUI primitive + "Why Triforce" section on/private-jetsand/ar/private-jets(src/components/ui/comparison-matrix.tsx,src/app/private-jets/page.tsx,src/app/ar/private-jets/page.tsx)A new
ComparisonMatrixcomponent added to the/ui/primitive library alongsideAccordion,Tabs, andTooltip. 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), ortext(plain copy). One column can be markedhighlight: trueto 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-jetsand 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-glasscard container withliquid-gradientaccent swell in the corner, matching the Accordion and Tabs visual language. Highlighted column tintedvar(--accent)/4%rising tovar(--accent)/7%on row hover — present without shouting.overflow-x-autowrapper withmin-w-[400px]on the table allows the full layout to render unclipped on every supported viewport width.docs/DESIGN_SYSTEM.mdupdated:ComparisonMatrixadded to the component catalogue under § 5 Components.Residual Value & Depreciation Curve on
/financingand/ar/financing(src/components/triforce/residual-value-chart.tsx)A new
ResidualValueChartsection 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 newresidual.*keys added to both the English and Arabic dictionaries (15 keys insrc/lib/i18n.ts). Arabic copy written for the reader (GCC buyer persona), not translated word-for-word. - Aircraft selector is a
<select>rendered attext-base(16px) — compliant with the iOS Safari auto-zoom rule. - Responsive: SVG
viewBoxscales to container width on all three breakpoint tiers; summary table isoverflow-x-autoon mobile. - Liquid-Glass styling:
liquid-glasscard,liquid-gradientambient swell,var(--accent)for the chart line and area fill, consistent with existing/financingpage design. docs/FINANCING.mdupdated: "Add depreciation curve" removed from follow-ups; component architecture section updated.
Structured data:
MedicalOrganizationschema + credential signals (src/app/layout.tsx)The root
organizationLdblock 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.hasOfferCatalogwith twoServiceoffers (Air Ambulance & Medevac; Private Jet Charter), eachurl-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.mdupdated to reflect the new schema shape.
Fixed
1 entryA11Y: 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 carriedrole="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 asaria-dialog-nameand focus-order failures).- New
useFocusTraphook (src/lib/use-focus-trap.ts) extracts the focus-trap logic that was already present insrc/components/ui/modal.tsxinto a reusable hook: auto-focuses the first focusable child on open; Tab / Shift+Tab cycle within the panel; Escape calls the optionalonClosecallback; focus returns to the previously-active element on close. - Applied to both dialogs in
FlightSimulator. Each dialog now also usesaria-labelledbypointing to its headingid(instead ofaria-labelwith a bare translation string). - No visual change; no impact on any other route or component.
- New
Added
3 entriesResponse Timeline —
/air-ambulanceand/ar/air-ambulanceThe 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
ResponseTimelinecomponent (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-gradientsheen overlay, and accent-tinted time badge to land the headline claim visually.Design:
- Server component — zero JS weight; scroll animations via the page-level
RevealOnScrollobserver and the existingios-reveal/ios-staggerCSS. - 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-directionreversal. - Fully bilingual — step text embedded as
{ en, ar }pairs in the component (same pattern asPressBand); section-level copy wired throughgetTranslator. - Four new i18n keys added to
src/lib/i18n.ts:responseTimeline.{eyebrow,heading,sub,ariaLabel}.
- Server component — zero JS weight; scroll animations via the page-level
Press Band wired into home pages —
/and/arPressBand(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-ambulanceThe 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 theTestimonialscomponent; they are now rendered on the Arabic air-ambulance page after Popular Routes.
Changed
1 entryOutline button hover glow —
Buttoncomponent,globals.cssAdded
.btn-outline-glowto theoutlinebutton variant. The primary CTA already had abtn-primary-glowthat 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. Thetransition-all duration-300already on the base button class animates the shadow; no extra JS. Colour tracks--accentso it works identically in the ambulance (red) and charter (gold) verticals, and in both dark and light mode.
Added
2 entriesMedical Capabilities section —
/air-ambulanceand/ar/air-ambulanceThe 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
MedicalCapabilitiescomponent (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-cheapopacitytransition) 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 accentShieldCheckicon. - Server component — no
"use client", no JavaScript payload beyond what Tailwind'stransition-opacityhandles 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
localeprop without requiring new i18n dictionary keys (the labels are short and stable enough to inline). - Wired into
/ar/air-ambulanceimmediately after the operational band, matching the English page structure exactly. data-keep-ltrapplied to aircraft tail/type strings so Latin model names render correctly inside the RTL Arabic layout.
Placement: between the
OperationalBandand theFeatureRowon both the English and Arabic air-ambulance landing pages. It answers the clinical question before the fleet and capability bands answer the operational one.- Glass cards (
Legal Notice page (
/legal/notice+/ar/legal/notice) — company transparency for due diligenceA $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.legalNoticei18n key in both locales). - Sitemap (
/app/sitemap.ts) emits/legal/noticewithlocalized: true, generating both English and Arabic<url>entries with hreflang alternates. - Middleware (
AR_BUILT_ROUTES) registers/ar/legal/noticeso 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 entryA11Y: 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 theLegalPagecomponent), 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 insrc/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
enandarlocales.
- Added
Added
1 entryAircraftStickyCTA— sticky enquiry bar on all aircraft detail pagesA 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 globalBottomNav(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-gradientsheen +btn-primary-glow, consistent with the navbar island and card surfaces. - RTL-safe:
dirattribute, logical utilities, arrow direction flips for Arabic (ArrowLeftinstead ofArrowRight). prefers-reduced-motion: transition stripped tonone.- Ambulance variant shows "Patients" label; charter variant shows "Passengers".
- All copy routed through
getTranslator(locale)using existing i18n keys.
- Wired into all four aircraft detail routes:
Changed
1 entryPolish: 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-2via a sharednavLinkconstant on every footer<Link>. Also upgraded the hotline<a>withhover:opacity-80and the "All Numbers" + version links with matchingtransition-colors+ underline. Applies to both EN and AR footers via the sharedSiteFootercomponent.
Added
2 entriesFleetExplorerfull Arabic localisation — interactive search & filter on all Arabic fleet pagesBoth Arabic fleet listing pages (
/ar/private-jets/fleetand/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 toDictionaryunder thefleetExplorer.*namespace:searchLabel,searchPlaceholder,filtersAriaLabel,filterByType,noResults,showingResult, and sixfilter.*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 optionallocaleprop (defaults to"en"for zero-change backward compatibility). All previously hardcoded English strings are now sourced viagetTranslator(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 staticAircraftCardgrid 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.tsxandsrc/app/air-ambulance/fleet/page.tsx— explicitlocale="en"prop passed toFleetExplorerfor clarity (behaviour unchanged).
docs/I18N.md—FleetExplorerfollow-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 indocs/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 entryA11Y: 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"andaria-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— addedfullscreenTriggerRef(captures the element that opened the dialog so it can receive focus back on close) andfullscreenDialogRef(scopes the Tab trap). A newuseEffect([fullscreenOpen])moves initial focus to the first focusable child 40 ms after the dialog opens and restores the trigger on close. AonFullscreenKeyDowncallback wraps Tab / Shift+Tab so keyboard users cycle within the panel instead of leaking out into the underlying page.
ArQrDialog— addedpreFocusRef,closeButtonRef, anddialogPanelRef. A mount-onlyuseEffect([])saves the active element, moves focus to the close button after 40 ms, and restores focus on unmount.onPanelKeyDownprovides the same Tab-trap logic. The existing ESC / scroll-lockuseEffect([onClose])is unchanged.
Added
2 entriesArabic testimonials in
<Testimonials>componentThe Testimonials component previously rendered the same English-language quotes regardless of locale. Arabic-speaking GCC buyers visiting
/ar/private-jetsor 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) andCHARTER_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 existinglocaleprop, so no call-site changes are required: all AR pages that already passlocale="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/managementAdded 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 taggeddata-keep-ltr. All directional icon usage corrected (ArrowLeft). RTL-safe logical Tailwind utilities throughout.Cross-links added:
/private-jets/acquisitionCTA footer now includes "Management services →" link./ar/private-jets/acquisitionCTA footer now includes "خدمات إدارة الطائرات" link.
Infrastructure updates:
src/middleware.ts—/ar/private-jets/managementadded toAR_BUILT_ROUTES.src/app/sitemap.ts—/private-jets/managementadded withlocalized: true,priority: 0.8,changeFrequency: "monthly".
Changed
1 entryTestimonial proof-claim highlight —
src/components/triforce/testimonials.tsx,src/app/globals.cssUpgraded the inline
<Proof>span fromfont-mediumat full foreground opacity tofont-semiboldwith a new.text-proofCSS 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 entriesSelf-hosted branded OG / social-preview image —
src/app/opengraph-image.tsx,src/lib/site.tsAll 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 RouterImageResponseroute that statically pre-renders a 1 200 × 630 branded PNG at build time, served directly from the CDN at/opengraph-image. UpdatedSITE_OG_IMAGEinsrc/lib/site.tsto${SITE_URL}/opengraph-imageso all pages that explicitly setopenGraph.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 ofopengraph-image.tsx. UpdatedSITE_OG_IMAGE_ALTto reflect the actual generated image content.No new packages required —
next/og(ImageResponse+ Satori + Resvg) ships with Next.js 16. The route is○ Staticin the build output — no per-request overhead.Lazy-load overlay UI components —
src/components/triforce/overlay-shell.tsxChatBot,CookieConsent, andInstallPromptwere 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 thatnext/dynamicwithssr: falseis legal in Next.js 16 App Router) that lazy-loads all three vianext/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.tsxAdded
<link rel="preconnect">and<link rel="dns-prefetch">forimages.unsplash.comto 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 entryAdmin settings selects —
src/app/admin/settings/settings-tabs.tsxTwo
<select>elements (Default vertical, Session timeout) usedtext-sm(14 px). CLAUDE.md mandates ≥ 16 px on all form inputs to prevent iOS Safari's "zoom on focus" behaviour. Changed both totext-base(16 px). Applies to admin-only surfaces per the standing rule.
Security
3 entriesPHI/PII guard — charter and air-ambulance API routes
POST /api/request/charterandPOST /api/request/air-ambulancepreviously fell through to an unguardedconsole.logfallback whenRESEND_API_KEYwas 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 insrc/lib/auth/email.ts. Operators who intentionally run without Resend on a staging deployment can setAUTH_LOG_LINK_FALLBACK=1to opt back in. In production without that flag the route returns502 resend_not_configuredinstead of logging PII/PHI.The account-deletion route (
DELETE /api/auth/account) changed its operational alert fromconsole.logtoconsole.warn(the appropriate level for "operator action required") to align with the new ESLint rule.ESLint
no-consolerule —eslint.config.mjsAdded
"no-console": ["error", { allow: ["warn", "error"] }]to the project ESLint config.console.logandconsole.debugare now lint errors;console.warnandconsole.errorremain permitted for operational alerts. This creates a lint-time barrier against future accidental PII leakage through unguarded debug logging./.well-known/security.txt—public/.well-known/security.txtAdded a RFC 9116-compliant
security.txtfile so security researchers have a clear responsible-disclosure path. Contact issecurity@triforce.flights; expires 2027-05-29; canonical URL and privacy policy linked.
Changed
2 entriesTestimonials — proof-phrase emphasis —
src/components/triforce/testimonials.tsxKey proof claims within testimonial quotes now render in full-opacity medium-weight text (
font-medium text-[var(--color-fg)]) against the card's default/85opacity 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; theTestimonial.quotefield is widened fromstringtoReactNodeto accept inline JSX fragments.A11Y: Footer navigation landmarks —
src/components/triforce/site-footer.tsxWrapped 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-coreregionrule). The visual design is unchanged.Added two i18n keys —
footer.navServicesLabel/footer.navAccountLegalLabel— in both English and Arabic toDICTIONARIESinsrc/lib/i18n.ts. TheDictionaryTypeScript type enforces the keys at compile time.
Added
5 entriesHome Page FAQ section —
src/components/triforce/home-faq.tsxAdded 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
Dictionarytype and bothenandardictionaries insrc/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-pagewrapper and Tailwind responsive utilities. - Section
aria-labelledbypointing at the visible heading for screen-reader landmark navigation (consistent with the rest of the home page sections).
- FAQ Page JSON-LD structured data embedded via
Acquisition Portfolio section —
src/components/triforce/acquisition-portfolio.tsxAdded a "Selected Transactions" credentials section to the Aircraft Acquisition Advisory page (
/private-jets/acquisitionand/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.tsxAdded 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 acceptReactNodebody 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.collapsibleprop (defaulttrue) — controls whether an open item can be re-collapsed whentype="single".defaultValue— string or string array to pre-open panels.disabledper-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>witharia-expandedandaria-controls; each panel hasrole="region"andaria-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 underprefers-reduced-motion. - Glass styling:
.liquid-glasscontainer,.liquid-gradientaccent sheen,CaretDownfrom@phosphor-icons/react— matches the existing FAQ accordion and navbar chrome exactly. - RTL-safe: sheen positioned with logical
end/topvalues; noleft/rightassumptions.
Wire-in:
src/app/services/page.tsx— new "Client questions" section (between Heritage and Independence Note) usingtype="single"with five items whose body content includes links and multi-paragraph prose, demonstratingReactNodecapability.src/app/ar/services/page.tsx— full MSA Arabic counterpart; RTL layout,data-keep-ltron any Latin runs, links point to/ar/*routes.
Aircraft Sales & Brokerage —
/private-jets/brokerage+/ar/private-jets/brokerageThe 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 viaAcquisitionTimeline, a specialist aircraft-types grid, a seven-question FAQ with JSON-LDFAQPageschema, 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 andArrowLeftCTAs,data-keep-ltron aviation model names and numeric statistics,og:locale=ar_SAand hreflang alternates wired viabuildHreflangAlternates.
src/middleware.ts—/ar/private-jets/brokerageadded toAR_BUILT_ROUTESso the Arabic URL is served rather than 307-redirected to its English equivalent.
src/app/sitemap.ts—/private-jets/brokerageadded withlocalized: trueandpriority: 0.8, causing the sitemap to emit both English and Arabic entries with hreflang alternates.
docs/I18N.md— coverage matrix updated:/private-jets/brokeragenow✅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— extendedNewsArticlewith an optionalar?: ArNewsTranslationfield. 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), andformatNewsDateAr(iso)(usesar-SAlocale for date formatting, e.g. "٨ مايو ٢٠٢٦").
src/lib/i18n.ts— twelve newDictionarykeys 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 passinglocale="ar"toSiteHeader,SiteFooter, andBottomNav.
src/app/ar/news/page.tsx— Arabic newsroom index, mirroring the English index withlistNewsAr()data, Arabic date formatting,og:locale=ar_SA, and correctMetadata.alternates.languagespointing back to/news.
src/app/ar/news/[slug]/page.tsx— Arabic article detail page withgenerateStaticParams()(only articles withartranslations are built),getNewsAr(slug)data, RTL-safeborder-s-2blockquote,data-keep-ltron Latin tags, JSON-LDNewsArticlewithinLanguage: "ar", and cross-locale hreflang alternates.
src/middleware.ts—/ar/newsadded to bothAR_BUILT_ROUTES(exact index match) andAR_BUILT_PREFIXES(covers/ar/news/[slug]). Previously any/ar/news/*path 307-redirected to the English equivalent.
src/app/sitemap.ts—/newspromoted tolocalized: true; each article now emits both an English and an Arabic sitemap entry with fullalternates.languagesso 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 setMetadata.alternates.languagesviabuildHreflangAlternatesso the<link rel="alternate" hreflang>tags are present on the English side too.
docs/I18N.md— coverage matrix updated:/news + articlesnow✅for both English and Arabic.
Fixed
3 entriesI18N: locale-aware 404 and 500 error pages for Arabic users
The
not-found.tsx(404) anderror.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
Dictionarykeys added tosrc/lib/i18n.tsunder a// Error pagessection (notFound.*anderror.*), with full English and Modern Standard Arabic translations. TypeScript's dictionary completeness check (bothenandarmust satisfy theDictionarytype) enforces parity at compile time. not-found.tsxnow deriveslocalefrom thex-pathnameheader (already forwarded by middleware on every request), callsgetTranslator(locale), and renders all user-visible strings throught(...). The "Return home" and "Contact us" links are also locale-aware (/arand/ar/contactfor Arabic sessions).error.tsx(client component) usesusePathname()+detectLocale()— the same pattern asCookieConsent— to derive locale at render time, then callsgetTranslator(locale)for all strings. The locale is also passed toSiteHeaderso 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-ltrso the hotline number renders correctly in the RTL context.- Twelve new
A11Y: fix WCAG AA contrast failure for
--color-fg-subtlein light modeThe light-mode token
--color-fg-subtlewas#6b7280(Tailwind gray-500), which produces only 3.9 : 1 contrast against the page background#f5f6f8and 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'scolor-contrastrule 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#f5f6f8and ≈ 5.3 : 1 on white card backgrounds, clearing the AA threshold with comfortable margin. Dark-mode consumers are unaffected (dark-mode--color-fg-subtleretains#6b7280, which passes at 4.7 : 1 on the near-black#07090cbackground).A11Y: expose the final stat value to assistive technology in
StatRevealThe count-up animation in
src/components/triforce/stat-reveal.tsxmutated 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
StatCellnow carries anaria-labelon its wrapper containing the final, stable value (e.g."2400+ — Missions flown. 2003 – present"). The animated<div>and its children are markedaria-hiddenso assistive technology reads the container label only, never an in-progress count.
Added
2 entriesPressBandcomponent — press & recognition marquee sectionNew
src/components/triforce/press-band.tsx— a CSS-animated horizontal marquee of editorial recognition cards (outlet name, category, pull quote, year). Follows the sameios-marquee+cert-band-maskpattern asCertificationBandso 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-ltrto preserve Latin rendering. pressBand.*i18n keys added to theDictionarytype and bothen/ardictionaries — TypeScript compile enforces completeness.- Wired into
/private-jets(EN) after the testimonials block, and into/ar/private-jets(AR) in the same position.
Missing
Testimonialsblock restored on/ar/private-jetsThe Arabic private-jets page was missing the charter testimonials section present on the English equivalent. Added
<Testimonials vertical="charter" locale="ar" />alongside the newPressBand, closing the content parity gap.
Security
10 entriesCSP: remove
unsafe-evalfrom production; addCross-Origin-Opener-Policy; expandPermissions-PolicyThree 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-evalwas only ever needed by webpack's HMR hot-reload, so the directive is now conditionally included only whenNODE_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-popupsis chosen over the strictersame-originso 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 forpayment,usb,serial,battery,ambient-light-sensor,accelerometer,gyroscope, andmagnetometer.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-selectto globals.css; fix missing focus ring on aircraft enquiry modal.form-inputwas previously defined only inside<style jsx global>blocks inair-ambulance-intake.tsxandtrip-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 sawfocus:outline-nonewith 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-inputand.form-selectintosrc/app/globals.cssas permanent@layer utilitiesentries, ensuring the styles are available on every route. The focus state now consistently delivers the accessible 3 pxbox-shadowring (28% opacity accent hue) plus border-color change in both dark and light modes. The duplicate<style jsx global>blocks inair-ambulance-intake.tsxandtrip-builder.tsxare 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 hadtransition: transform 1100ms, opacity 700msin 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-cardto the global reduced-motion override inglobals.css.2.
install-slide-up(install-prompt.tsx) — the PWA install banner entrance keyframe included atranslateY+scalethat 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 usedtranslateY(10px)on every tick. Underprefers-reduced-motion, it now holds a static mid-opacity state instead.4.
fade-inoverlays (zoomable-image.tsx,aircraft-hero.tsx) — three overlay/dialog containers usedanimate-[fade-in_180ms_ease-out]unconditionally. Changed tomotion-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
Buttoncomponent's hover state wasbrightness-110plus 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-shadowglow (colour-mixed fromvar(--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-accentCTA. The hover bloom is animated by the existing global button transition rule (220 ms easebox-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:hoverutilities inside@layer utilities; box-shadow transition provided by the existing unlayered global button rule.
src/components/ui/button.tsx— primary variant addsbtn-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 was0.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 to0.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 was14px, more than double the8pxused 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 to8pxto match the system-wide reveal standard: the text now resolves crisply within the same animation duration.Homepage stat grid: fix orphaned
border-ldividers + deepen liquid-gradient accentThe
StatRevealcomponent usedfirst:border-l-0to 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-lre-added the border to the first cell afterfirst:border-l-0removed it, producing an orphaned vertical line at the start of the 4-column row.Fix: replaced the
first:selectors withodd: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-25toopacity-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
/arThe
ScrollStorycomponent was the last English-only section on the Arabic home page, explicitly flagged as a follow-up indocs/I18N.md. GCC buyers arriving at/arwere 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.
ScrollStorywas stateless-hardcoded English. It now accepts an optionallocale?: Localeprop (defaults to"en"so all existing call sites are unaffected). AbuildChapters(t)helper constructs the five-chapter data array from the translator, keeping locale logic out of JSX. ThegetTranslatorcall 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-chaptereyebrow,title,body, andaltfor all five chapters, including a translatable stamp for chapter five (scrollStory.alwaysOn.stamp→دائماً). All 31 keys are fully typed inDictionary— 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 carryrtl:tracking-normal— letter-spacing on connected Arabic script breaks ligatures and looks broken. The mobile chapter-stamp position changed fromleft-4tostart-4(logical property, mirrors correctly in RTL). Reduced-motion and IntersectionObserver scroll-spy are locale-agnostic and untouched.5. Decorative marquee divider added to
/arbetween ScrollStory and the stat block, matching the English home page's visual rhythm. Uses Arabic copy (وحدة عناية مركزة في الجوّ,غرفة العمليات,عالمياً,إقلاع في ٤٢ دقيقة) with Eastern Arabic-Indic numerals (٢٤ / ٧ / ٣٦٥). Theios-marqueeclass automatically animates right-to-left onhtml[dir="rtl"]via the existingios-marquee-rtlkeyframe inglobals.css.Coverage impact: The
/arhome page now matches the/home page section-for-section.ScrollStoryis removed from thedocs/I18N.mdfollow-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-jetsand/ar/private-jetsGulf 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-ltrso numeric values render correctly inside Arabic prose.
- Design system compliant: Liquid-glass card surfaces (
.liquid-glass), rotating accent sheen (.liquid-gradient),liquid-glass-chipicon badges,var(--accent)colour theming,ios-reveal/ios-staggerentrance animations,font-hero/font-displayfor headings. Consistent with the operational-band and range-planner components in the same page.
- Placement: Added to
/private-jetsand/ar/private-jetsbetween 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-onlyspans), 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-jetsand/ar/private-jetsA 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, noml/mr), and theAirplaneTilticon is rotated 180° on the Arabic route-summary row. Airport IATA codes carrydata-keep-ltrso 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 againstrangeNm(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-upentrance animation via the existing page-levelRevealOnScrollobserver. Results panel isaria-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 entriesRate 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. Returnsok: boolean,remaining, andresetAtfor 429 response headers.Limits applied:
POST /api/auth/request— 5 requests / IP / 15 min (matches magic-link token TTL; prevents email-bombing through Triforce's sending domain)POST /api/request/charter— 10 requests / IP / hour (generous for legitimate broker/advisor use; hard barrier for automated flooding)POST /api/request/air-ambulance— 5 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, andX-RateLimit-Resetheaders 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:
submittedAtin 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: swapto root fontsA11y — admin select inputs. Four
<select>elements across three admin pages (/admin/inbox,/admin/audit,/admin/analytics) were sized withtext-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 totext-base(16px). The two controls in audit and analytics also hadh-9(36px) — too short to comfortably render 16px text with padding — bumped toh-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/fontdeclarations insrc/app/layout.tsx(Inter and Cormorant Garamond) lacked an explicitdisplayoption. Without it Next.js may emitfont-display: optionalorblockfor variable fonts depending on the version, which causes invisible text during font load (FOIT) — a Lighthouse Performance flag and a CLS contributor. Addingdisplay: "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-staggerchoreography 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 receiveios-reveal; the<h2>receivesios-reveal-up; in grid layout the card wrapper grid getsios-staggerand each card receives anios-revealwrapper so the six testimonial cards cascade in at 60 ms intervals (60 / 140 / 220 / 300 / 380 / 460 ms) via the existingglobals.cssstagger delays. In rail layout (home page), the CardRail wrapper receivesios-revealso the whole rail fades in together. All animations respectprefers-reduced-motionvia the existingRevealOnScrollobserver that immediately addsis-infor 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-jetsand/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 i18nsection.featured.titlekey is updated in bothenandardictionaries; 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 purchaseThe 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 viastart-0logical 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 filledCheckCircleicons. RTL-safe throughout (logical Tailwind utilities:ps-*,start-*,end-*). Entrance animation via page-levelRevealOnScroll—ios-revealon each<li>. The vertical line usescolor-mixin 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-ltron the stats values. RTL-safe layout identical to the English page via logical utilities.
src/middleware.ts—/ar/private-jets/acquisitionadded toAR_BUILT_ROUTESso the 307 fallback is lifted.
src/app/sitemap.ts—/private-jets/acquisitionadded withpriority: 0.8, localized: trueso both/private-jets/acquisitionand/ar/private-jets/acquisitionappear in the XML sitemap with<xhtml:link>hreflang alternates.
src/app/private-jets/page.tsxandsrc/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 coverageThe English
/legal/medical-disclaimerwas 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-disclaimeradded toAR_BUILT_ROUTESso the 307 fallback redirect to English is lifted.src/app/sitemap.ts—/legal/medical-disclaimerflipped tolocalized: true; the XML sitemap now emits both/legal/medical-disclaimerand/ar/legal/medical-disclaimerwith proper<xhtml:link>hreflang alternates.src/app/legal/medical-disclaimer/page.tsx— hreflangalternates.languagesadded 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-disclaimernow 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
Tabscomponent was missing all WAI-ARIA semantics: norole="tablist"on the container, norole="tab"on individual buttons, norole="tabpanel"on content regions, noaria-selected, noaria-controls/aria-labelledbypairing, 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 carryrole="tab",aria-selected,aria-controls. Panel divs carryrole="tabpanel",aria-labelledby,hiddenon inactive panels. The tablist enforces a single tab stop:tabIndex={0}on the active tab,tabIndex={-1}on all others. Keyboard:ArrowLeft/ArrowRightcycle focus and activate the tab (follows-focus pattern, per APG).Home/Endjump to first/last. RTL-aware:ArrowRightadvances in LTR, retreats in RTL — controlled by a newdirprop.Liquid-glass pills variant. Added
variant?: "underline" | "pills"prop (default"underline"for backward compatibility). Thepillsvariant renders a frosted-glass pill strip —liquid-glasscontainer,liquid-glass-chipactive tab, spinningliquid-gradientaccent 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 asvariant="pills"(the Arabic page also getsdir="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 usestypeLabelAr,taglineAr,toLocaleString("ar-SA"), anddata-keep-ltron numeric values. The section is conditionally rendered (no similar aircraft = no orphan heading). New i18n keysaircraft.similar.eyebrowandaircraft.similar.headingadded to both EN and AR dictionaries; the compile-timeDictionarytype enforces no missing keys.New utility (`src/lib/aircraft.ts`).
getSimilarAircraft(slug, limit)returns up tolimitaircraft 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 bandThe 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.tsx—TooltipandTooltipProviderprimitives. Portal-based (createPortal → document.body) so the bubble is never clipped byoverflow:hiddenancestors (e.g. marquee strips). Positions viagetBoundingClientRectat hover time and repositions on scroll/resize. Supports logical placementstop | bottom | start | end(start/end resolve to left/right based ondocument.documentElement.dir), full RTL-aware. Keyboard-focusable wrapper (tabIndex=0) by default; passkeyboard={false}for already-focusable triggers to avoid double focus stops. Wired toaria-describedby+role="tooltip"for WCAG 2.1. Escape key closes. Entrance animation via newtooltip-inkeyframe using the CSSscaleindividual-transform property (composes cleanly withtransform: translate(…)inline style).motion-safe:variant wraps the animation to respectprefers-reduced-motion.src/app/globals.css—@keyframes tooltip-inentrance animation (opacity + CSSscaleproperty). 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 restingbox-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 inmotion-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 surfaceThe English
/care-teampage 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-ltron 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-teamadded toAR_BUILT_ROUTESso the fallback redirect is lifted.src/app/care-team/page.tsx— hreflang alternates added (the English page was missingbuildHreflangAlternates("/care-team"), meaning Google could not discover the Arabic counterpart via on-page signals).src/app/sitemap.ts—/care-teamflipped tolocalized: trueso the XML sitemap emits both/care-teamand/ar/care-teamwith proper<xhtml:link>alternates.docs/I18N.md— coverage matrix updated;/care-teamnow 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
backgroundImage→next/imagewithpriority(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-imagestring. 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, responsivesrcset) was bypassed, leaving the browser downloading a full-resolution JPEG. Both hero images are hosted onimages.unsplash.com, which is already listed innext.config.tsremotePatterns, 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 (sameopacity-25,object-cover,object-centerbehaviour) but now participates in Next.js optimisation. Thequality={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-hiddenmoved 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/fleetand/air-ambulance/fleethad 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: addedaria-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-hiddeninside already-labelled controls.
Added
1 entryGlass 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.bodyviacreatePortal, 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-glasschrome,.liquid-gradientsheen atopacity-60, top highlight line,shadow-[0_32px_80px_-16px_rgba(0,0,0,0.72)]. Accessibility:role="dialog",aria-modal="true",aria-labelledby/aria-describedbyfrom auto-generated unique IDs, full Tab trap (cycles focus within panel at both boundaries), Escape key and scrim click both callonClose,<body style="overflow:hidden">on open, focus restored to the trigger on close (WCAG 2.4.3). Animation:@keyframes modal-slide-upadded toglobals.css—opacity: 0 + translateY(20px) + scale(0.98)→ resting state over0.28swithcubic-bezier(0.22,1,0.36,1)(Apple spring feel);prefers-reduced-motionoverride reduces the keyframe to a plain cross-fade. SSR-safe:mountedguard preventscreatePortalfrom 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
CalendarCheckicon, 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-inpututility (16 px font, no iOS Safari zoom), required-field asterisks witharia-label, and anaria-live-compatible success state. On submit the component shows aCheckCircleconfirmation panel; in production, swap thesetTimeoutstub for a real CRM/email endpoint. Locale-aware: readsdocument.documentElement.dataset.locale(same pattern asCommandPalette) with aMutationObserverfor SPA navigation; or accepts alocaleprop 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]) receiveslocale="ar"explicitly so the server-rendered page drives locale without depending on the DOM mutation. RTL layout: logicalms-*spacing,flex-row-reverseon the card row,rotate-180on the directional arrow icon,text-endon body copy,dir="ltr"locked on email/phone/date inputs (Latin character strings).Build: Both
private-jets/fleet/[slug]andar/private-jets/fleet/[slug]build clean (193 routes, no TypeScript errors). No newpnpmdependencies — the component uses only already-present@phosphor-icons/reacticons and existing design utilities.
Changed
1 entryStackingCards — 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
700mscubic-bezier transition onbox-shadowandborder-color: on hover the shadow deepens (0_50px_120px_-15px_rgba(0,0,0,0.85)from0_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 inmotion-safe:— users withprefers-reduced-motionsee no animation. No structural changes; one element, six utility classes.
Added
1 entryEmpty Leg Flights page —
/empty-legsand/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-jetspage 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-glassleg cards showing origin → destination airport pair, ICAO codes, date, aircraft, flight time, price-from, saving percentage, and a direct request CTA linking to/request/charterwith pre-filledrouteandaircraftquery 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 withuseState; no URL params needed for this inventory size.Design:
liquid-glasscard treatment with.liquid-gradientaccent,AirplaneTilticon (RTL-flipped viarotate-180), accent-coloured badge chips,font-displayroute typography,rounded-2xlCTAs, three-column grid on desktop (1 → 2 → 3 cols across breakpoints).Bilingual: 29 new
Dictionarykeys 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-ltron airport codes, aircraft names, prices, and flight times;localizePathon all internal links; RTL-aware icon rotation and layout direction./ar/empty-legsregistered inAR_BUILT_ROUTESinsrc/middleware.ts;/empty-legsadded tosrc/app/sitemap.tswithlocalized: trueandchangeFrequency: "daily"(inventory changes daily); hreflang alternates wired viabuildHreflangAlternates.docs/I18N.mdcoverage matrix updated.
Security
1 entryPostCSS CVE GHSA-qx2v-qp2m-jg93 — pin
postcssto>=8.5.10viapnpm.overrides(package.json)next@16.2.6ships a transitive dependency onpostcss@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 runningpnpm auditas 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 duringnext 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" } }topackage.jsonforces the override, eliminates the duplicate resolution, and makespnpm auditreturn No known vulnerabilities found. Build verified clean (191 pages, TypeScript pass).
Fixed
1 entryArabic 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
/privacyand/terms(English routes) instead of/ar/privacyand/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 entriesA11Y: CommandPalette — focus trap,
inertbackground, 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 underaria-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 trap —onKeyDownon the dialog panel queries all focusable descendants and cycles focus at both boundaries (first ↔ last), identical to the existing pattern inChatBot; (2) `inert` on background —.splash-contentreceives theinertattribute while the palette is open so AT users cannot navigate outside the modal; (3) focus-return —preFocusRefcapturesdocument.activeElementat 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-heroweight 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 appliesfont-weight: 900,letter-spacing: -0.035em, andline-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.98is _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-herofrom the body paragraph (reverting to the inheritedfont-sansat normal weight / default line-height) and addsleading-relaxed(1.625) explicitly so the prose breathes correctly. The headings retain.font-heroas intended — only the descriptive body copy is corrected.
Added
3 entriesCertificationBand 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 asliquid-glass-chippills — 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, andtext-accentcolouring that tracks the active vertical (red for ambulance, gold for charter). Animation uses the existingios-marqueeCSS 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-motionis handled by the existingglobals.cssrule (animation: none !importanton.ios-marquee); users who prefer reduced motion see the static badge list at translateX(0) filling the viewport. The.cert-band-maskutility (added to globals.css) applies edge-fade masking identical to the jet-marquee so the infinite loop is always seamless. Badge names carrydata-keep-ltrfor correct rendering inside the Arabic RTL layout. The marquee track itself carriesdir="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 bothenandardictionaries; theDictionarytype enforces completeness at compile time.Arabic Missions page —
/ar/missionsbrings the operational live-feed to GCC buyers (src/app/ar/missions/page.tsx)The coverage matrix showed
/missionsas 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.), ArabicfmtETAfunction (minutes → "دقيقة", hours → "ساعة"),data-keep-ltron 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 renamedroute-active-ar/route-recent-arto 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 anddata-keep-ltron 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 carrydir="ltr"since/ar/newsis not yet built, consistent with the documented omission pattern. RTL-safe throughout:lg:text-endon the ETA column (correct for RTL column ordering), logical utilities (ms-*,start-*,end-*),rtl:lg:divide-x-reverseon the stats band divider./ar/missionsregistered inAR_BUILT_ROUTES(src/middleware.ts) andsitemap.tsflaggedlocalized: true; English/missionspage gainsalternates.languageshreflang.docs/I18N.mdcoverage matrix updated:/missionsnow ✅ ✅.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/privacyand/termspages gainalternates.languageshreflang metadata (they were missing it), connecting them to the new Arabic counterparts in Google's hreflang graph. Both routes are registered inAR_BUILT_ROUTESinsrc/middleware.ts(removing the 307 fallback redirect) and insrc/app/sitemap.tswithlocalized: true(emitting parallel Arabic sitemap entries with hreflang alternates).docs/I18N.mdcoverage matrix updated:/privacy,/terms, and/cookiesnow 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 entryA11Y: 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 underaria-required-attranddialog-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 usesaria-labelledby="install-prompt-title"pointing to the card's visible<h2>, which is the preferred pattern overaria-labelper 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 (Tabwraps between the first and last focusable elements) and Escape closes the prompt with a snooze, matching the pattern already used inchat-bot.tsxand 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-labelledrole="dialog"— it is a single expandable button, not a dialog; changed torole="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 existingheadlinevalue which is already translated).
Added
1 entryCommand 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+Kfrom 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-gradientbackdrop layers,backdrop-blur-2xl backdrop-saturate-150,border-white/15, concentricrounded-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 + ArabiclabelAr, English hint + ArabichintAr). Search matches against all four fields so an Arabic user typing in English still finds results. Locale is detected at runtime fromdocument.documentElement.dataset.locale(the same attribute written byLocaleSync) viaMutationObserver, so locale changes on SPA navigation are reflected instantly without a re-mount. The active-row CaretRight flipsrotate-180in RTL; the⌘Khint and keyboard legend carrydata-keep-ltr; Arabic group labels (الصفحات,الأسطول,الخدمات) are rendered directly. Keyboard navigation:↑↓cycle the active row,↵navigates vianext/navigationrouter,Escapecloses. Active row scrolls into view on arrow key. Architecture: module-level singleton_open+ subscriber set (same pattern astoast.tsx— no React context, no provider needed anywhere). Exported:openCommandPalette(),closeCommandPalette(),toggleCommandPalette(),CommandPalette. Responsive:⌘Kpill shown only atlg+; icon-only chip shown at<lgso mobile users can tap to open too. Search input locked tofont-size: 16pxper the iOS Safari anti-zoom rule. Mounted once in root layout alongside<Toaster />.
Changed
1 entryScrollStory: 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 toscaleX(1)so the bar smoothly animates from 0% to 100% via the existing 900mscubic-bezier(0.16,1,0.3,1)transition when the chapter enters the viewport — matching the behaviour already used for theprefers-reduced-motionpath. Additionally, the fill colour is changed from plainbg-whitetobg-[var(--color-aa)](ambulance red, the brand accent for this section) and the track is softened frombg-white/15tobg-white/10so 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 entryArabic home page — ship
JetMarquee,StatReveal, andStackingCardsin 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
/arto feature parity with the English home:JetMarquee— acceptslocale?: Localeprop; auto-selectsaircraft.taglineArandaircraft.typeLabelArfrom the existing aircraft data (already bilingual); aircraft name and spec values wrapped indata-keep-ltrfor correct RTL rendering; fleet links route to/ar/private-jets/fleet/…vialocalizePath; heading, eyebrow, pause/play button text, and ARIA labels driven through the translator.StatReveal— acceptslocale?: Locale; stats array built insideuseMemofrom translator strings so TypeScript enforces completeness at compile time; certification abbreviations (ARGUS Platinum · IS-BAO Stage 3) carrydata-keep-ltrto protect brand names from RTL reflow inside Arabic prose.StackingCards— acceptslocale?: Locale; card copy (eyebrow, title, body, CTA, alt text) fully translated into MSA; service links uselocalizePathso/air-ambulance→/ar/air-ambulanceand/private-jets→/ar/private-jetsautomatically; the directional arrow icon flips fromArrowRighttoArrowLeftin RTL, and the hover translate animation reverses direction (-translate-x-1for RTL).- 37 new
Dictionarykeys added (JetMarquee × 8, StatReveal × 10, StackingCards × 16, image alts × 3) with both English and Arabic values; theDictionarytype makes any missing key a TypeScript compile error — zero runtime surprises. - All existing English home page callers are unchanged — the new
localeprop 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 entriesAdmin 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 convertspage.tsxto a Next.js App Router Server Component that callsprocess.envat request time to check for each integration's credential env var(s); the resolvedIntegrationEntry[]array is passed as a serialised prop to the extractedsettings-tabs.tsxClient 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 rulelandmark-uniquefires 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 addsaria-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 throughgetTranslator()/ theDictionarytype and covered by bothenandardictionaries — TypeScript will catch any future missing-key regressions at compile time.
Added
1 entryOperationalBandcomponent (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
StatRevealhomepage section (large-format display typography, fixed ambulance stats) —OperationalBandis space-efficient, vertical-aware, and fully bilingual. Counter animation fires on scroll-in viaIntersectionObserverrespectingprefers-reduced-motion. RTL-safe: logical Tailwind utilities throughout;data-keep-ltrapplied 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 entryFooter: 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 bothenandardictionaries; certification abbreviations carrydata-keep-ltrso they render correctly in RTL Arabic context.
Added
2 entriesArabic
/aboutand/servicespages (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_ROUTESinsrc/middleware.tsso they no longer fall back; (2) setMetadata.alternates.languagesviabuildHreflangAlternates; (3) markedlocalized: trueinsrc/app/sitemap.tsso Google discovers both/about+/ar/aboutand/services+/ar/serviceswith correct hreflang alternates. RTL-safe layout throughout:start-*/end-*logical utilities,data-keep-ltron IATA codes, year numbers, ICAO acronyms, and the phone number. Coverage matrix indocs/I18N.mdupdated.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 inAR_BUILT_ROUTESin middleware and insitemap.tswithlocalized: trueso both English and Arabic URLs appear in the sitemap with correct hreflang alternates. (3)<CookieConsent>— a lightweight Liquid Glass banner (iOS 26liquid-glassclass, backdrop blur, animated slide-up) wired into the root layout. Appears 1.8 s after first visit, persists the dismissal inlocalStorageso it never shows again, is locale-aware (links to/cookiesor/ar/cookiesdepending 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 bothenandarto 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 entryA11Y — mobile nav drawer focus management and
inertisolation (src/components/triforce/site-header.tsx,src/lib/i18n.ts)The hamburger menu used CSS
grid-rows: 0frto 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 carriesinert={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 markedinertso 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 gainsaria-haspopup="true"and the drawer getsrole="navigation"with a localisedaria-label(English: "Site navigation", Arabic: "التنقل في الموقع") added to both locale dictionaries ini18n.ts. No visual change; no reduced-motion regression; all three breakpoints unaffected (the menu islg:hidden).
Added
1 entryToast 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-aared 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 viamotion-reduce:hiddenfor vestibular-disorder users. Enter animation (translate-y-3 → 0 + opacity) and exit animation share the same 300mstransition-allguarded bymotion-reduce:transition-none.role="alert"/aria-live="assertive"for errors;role="status"/aria-live="polite"for everything else;aria-atomicso 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 usesend-*logical Tailwind utilities so the stack correctly appears at bottom-left in Arabic and bottom-right in English. Z-index9200sits above all page content and the ChatBot widget (z-[55]) but below the skip-to-content link (z-[9999]). Bottom offset usesenv(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-shrinkadded toglobals.css. Wired to two existing components for immediate visible impact:share-button.tsxnow callstoast.success("Link copied to clipboard")on clipboard write (replacing the invisiblearia-livespan that provided zero visible feedback);favorite-button.tsxcallstoast.successon save andtoast.infoon 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 entryButton — premium border radius, smooth transition, and richer hover states
The
Buttoncomponent (src/components/ui/button.tsx) previously usedrounded-md(6 px) on every CTA sitewide — a jarring mismatch against therounded-2xl/rounded-3xlglass cards androunded-fullnavbar pill that surround them on every page. Changed torounded-2xl(16 px) to bring buttons into alignment with the design system's soft-luxury language. Transition updated fromtransition-colors(no duration) totransition-all duration-300— all animatable properties now ease together on hover rather than snapping. Primary variant gainsmotion-safe:hover:-translate-y-px(1 px lift on hover, spring-out easing, gated behindprefers-reduced-motionso vestibular-disorder users see none of it) — the physical lift signals "interactive" without distracting copy. Outline variant gainshover:bg-accent-softso 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 carriesfocus-visible:ring-accenton 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-noneadded 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 entriesSafety & 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, anddata-keep-ltrguards on all Latin cert names and issuer identifiers. Route registered inAR_BUILT_ROUTES(middleware) andSTATIC_PATHS(sitemap,localized: true, priority 0.85). Footer link added to both locales. Sixteen new i18n keys added to theDictionarytype; TypeScript enforces completeness in both English and Arabic dictionaries.docs/I18N.mdcoverage 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. TheOrganizationblock (#organization) declares the brand entity: name, logo (SVG), OG hero image, full site description, twoContactPointentries (customer-service + emergency, both 24/7, both English + Arabic), and asameAsreference to@triforceaero. TheWebSiteblock (#website) links back to#organizationas publisher and declaresinLanguage: ["en", "ar"]. Both use stable@idanchors 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.mdupdated.
Fixed
1 entryA11Y —
prefers-reduced-motioncoverage for all inline animationsEvery
animate-spin,animate-ping, andanimate-pulseusage that lacked a reduced-motion guard now carries the appropriate Tailwind variant: loading spinners getmotion-reduce:animate-none(icon stays visible, rotation stops); decorative ping rings and status-dot pulses getmotion-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 entriesProcess Stepper — "How It Works" component
New reusable
ProcessStepperUI 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 existingios-reveal/ios-staggerscroll-driven system (relies on the page-level<RevealOnScroll />observer — no extra JS bundle). RTL-correct throughout via Tailwind logical utilities. Wired into both/private-jetsand/ar/private-jetsas 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-ltrguards on Latin numeric strings. A "Compare all aircraft" link added to both/private-jetsand/ar/private-jets. Route registered inAR_BUILT_ROUTES(middleware) andSTATIC_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 writesdocs/agendas/YYYY-MM-DD.mdtomain. Each file snapshots open PRs (draft vs ready, author, head branch), commits shipped tomainin the last 24h, open issues,claude/*branch sprawl, and the standing roadmap reminders fromCLAUDE.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 usesgh+jq, so it runs locally too (./scripts/generate-daily-agenda.sh [YYYY-MM-DD]). The workflow also exposesworkflow_dispatchwith an optionaldateinput for backfilling. Bootstrapped today's agenda by hand (docs/agendas/2026-05-15.md); subsequent days are auto-generated. Index lives atdocs/agendas/README.md.
Changed
2 entriesCapabilityBand + 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-2xlglass surface with inset rim highlights, a subtle spinningliquid-gradientaccent orb at top-right, and a glass-chip circle icon container. The flatborder-ydivider is replaced by the cards' inherent elevation. FeatureRow icon containers are similarly upgraded from basicrounded-md bordersquares toliquid-glass-chip rounded-xlpills. (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-gradientorb — 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 entryPrivate 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 entriesSimulator: 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.flightsReplaced the previous
.aeroorigin 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 aHEADpre-check (some hosts returned 405 / wrongContent-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 8× 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 entriesSimulator: 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 Viewerar_onlyintent from the GLB, iOS redirects to Quick Look when the endpoint confirms the USDZ is available, and fallback page loads carryar=1so the 3D viewer activates immediately. The QR dialog is portaled todocument.bodyso 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-srcnow permitsblob: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.yon 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-slugrotorMainSpinAxisoverride inGLB_FIXUP).
Added
1 entrySimulator → 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 proxyGET /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; respectsprefers-reduced-motion(slow static overview) and low-power devices. Docs:docs/WORLD_TOUR.md.
Fixed
6 entriesSimulator: 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.tsand animated insim-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 atz-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 atz-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 atmd(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: π/2fix-up laid it across the runway; the fix-up is nowrotY: π. 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_modelroot-node matrix, so the per-slugrotX: -π/2fix-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 nowrotY: π(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/2utilities with a JS-managedtransform. Under Tailwind v4 those utilities emit thetranslateCSS property, which is *additive* totransform, 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/JStransform(src/components/simulator/hud/touch-controls.tsx).
Changed
1 entrySimulator pages: disable text/image selection
The
/simulatorand/ar/simulator<main>wrappers now carryselect-noneplus-webkit-user-drag:noneon 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 entriesSimulator: 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 owntryLoadAircraftGLBnormalisation 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 andsessionStorage, 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; honoursprefers-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 flaggedkeepWithGlb, so it stays visible after the real Sketchfab GLB takes over the silhouette. Two new cached canvas textures (getFireCoreTexture,getFireRingTexture) freed viadisposeSharedSkins().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 nowtouch-action: noneso 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.
- 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
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 theaircraftsearch-param and pass it through to the client asinitialSlug;FlightSimulatorvalidates the slug againstgetAircraft(...)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 ofObject3D.rotationwrites 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 viaCanvasTexture, 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 insideSimCore, 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 tolocalStorage["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. Newcore/leaderboard.ts+hud/medal-badge.tsx. All new UI surfaces ship with Modern Standard Arabic copy (sim.medal.*,sim.leaderboard.*) in the same PR.
- First-person cockpit interior is now wired. The procedural PBR flight deck scaffolded in #145 (
Changed
1 entrySimulator: 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 respectsenv(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). Newsim.controls.throttleShort/sim.touch.stickstrings ship with Arabic copy.
Fixed
4 entriesTop progressive-blur band no longer veils on-page chrome
(
src/app/globals.css)..progressive-blur-topwasz-index: 30and is rendered after{children}in the DOM, so it painted *over* page chrome that also sits atz-30— most visibly the aircraft pages' sticky 2D/3D toggle pill, which looked blurred/dimmed. Dropped the band toz-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_localoffset 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 singlerotY: π/2to bring the already-Y-up model's nose to +X. The per-helicopter blanket denial and theairbus-h145entry 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 previousrotY: π/2fix-up was rotating around the nose-axis, which read as a 90° roll. Replaced withrotX: -π/2so the model converts Z-up → Y-up cleanly while the nose stays along +X.
Added
6 entriesSimulator: WebXR (VR) + spatial audio
(
src/components/simulator/**):- Enter VR from the toolbar.
SimCorenow probesnavigator.xr.isSessionSupported("immersive-vr")on startup and bubbles anxrSupportedevent up to React; when true, aVirtualRealitytoolbar chip appears next to the fullscreen button (EN: "Enter VR" / AR: "تفعيل الواقع الافتراضي"). One click opens an immersive-VR session viarenderer.xr.setSession(...)with optionallocal-floor,bounded-floor,hand-trackingandlayersfeatures — works on any WebXR-capable headset (Quest Browser, Vision Pro Safari, Pico, WMR, Index/Vive desktop Chrome/Edge). The render loop swapped fromrequestAnimationFrametorenderer.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
PerspectiveCameranow lives under aTHREE.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).
SimAudioroutes the engine bus through aPannerNode(HRTF, inverse rolloff, ref-dist 6 m) placed at the aircraft position, and updates theAudioListenerposition + 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.
- Enter VR from the toolbar.
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
.hdrasset, the same sky shader is rendered into a small dome inside a throw-away "env scene" and baked throughPMREMGenerator.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 undersim.tod.*(Dawn / Morning / Noon / Golden / Dusk / Night, Arabic:الفجر / الصباح / الظهيرة / الساعة الذهبية / الغسق / الليل).
- 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
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-stepFlightEnvironmentso 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 intosim-corebetween 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
FUELandGWchips 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.
- Per-aircraft published performance database (
Simulator: wheels feel the ground
(
src/components/simulator/**):- GLBs visibly sit on the runway, never in it. Per-slug
groundLiftbumped (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 insim-core.tsre-assertsbodyY ≥ heightAt(x,z) + gearLengthevery 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.
- GLBs visibly sit on the runway, never in it. Per-slug
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
gearLengthplus 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 incore/audio.ts.
- 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
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-motioncompliance 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 inAR_BUILT_ROUTES(middleware), added to the sitemap withlocalized: trueand 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/simulatoris 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/simulatorand/ar/simulator. Newsim.exitstring (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-inspeechSynthesis(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 inlocalStorage. - 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 entryprefers-reduced-motionhardening forScrollStoryandStackingCards(
src/components/triforce/scroll-story.tsx,src/components/triforce/stacking-cards.tsx).The
ScrollStorycomponent's sticky chapter-image panel drove its crossfade (opacity + 1.1 s scale + blur) through inline Tailwind transition classes and inlinestyleattributes. 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:
ScrollStorynow readsmatchMedia("(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.StackingCardshadtransition-transform duration-[1400ms] group-hover:scale-[1.04]andtransition-transform duration-500 group-hover:translate-x-1applied to child elements of a<Link>. The CSSa[href]:hover { transform: none }rule targets the anchor itself, not descendants, so both hover transforms were unreachable by the reset. Fixed with Tailwind v4motion-safe:variants so the transitions and transforms are only registered when the OS allows motion.
Added
2 entriesCardRailUI 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 fordir), respectsprefers-reduced-motion(instant instead of smooth scrollBy), and usesResizeObserverto keep the arrow disabled-state accurate.Testimonials on the home page
— the existing
Testimonialscomponent (previously only on/private-jetsand/air-ambulance) now renders on both/and/arusing the newlayout="rail"prop, placing six ambulance-vertical testimonials in aCardRailafter 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 thedata-keep-ltrexception — the quotes are attributed to named international clients and read naturally in English across the GCC professional context.
Changed
4 entriesHomepage 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) viahero.home.subtitlein 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.glband 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 viaMeshStandardMaterial.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/aircraftGLBs. - 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 behindnext/dynamic({ ssr: false }),prefers-reduced-motionopt-in, visibility-pause, full WebGL teardown on unmount. - Arabic mirror at
/ar/simulator(registered inAR_BUILT_ROUTES); all chrome/copy/buttons/results modal localised. In-canvas instrument shorthand stays in aviation English — seedocs/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 fromPROPOSAL.md(engineering scope) andPRD.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
Aircraftrecord:taglineAr(short marketing tagline),overviewAr(overview paragraph),typeLabelAr(category label, e.g. "طائرة ثقيلة"), andcapabilitiesAr(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 bothenandardictionaries; TypeScript'sDictionarytype enforces completeness. - `AircraftHero` already accepted a
localeprop — Arabic detail pages passlocale="ar"so annotation pins render in Arabic (titleAr/bodyAr). - Middleware (
AR_BUILT_PREFIXES) updated with/ar/private-jets/fleetand/ar/air-ambulance/fleetso these routes are served instead of 307-redirected to English. - Sitemap updated: fleet listing paths (
/private-jets/fleet,/air-ambulance/fleet) promoted tolocalized: true; all aircraft detail URL pairs (/en + /ar) now emitted withalternates.languagesfor Google hreflang discovery. - Hreflang alternates added to both English fleet listing and detail pages via
buildHreflangAlternates()so Google correctly understands the canonical → translated relationship.
- Built
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 by1/zoomso the dot and callout stay legible at any magnification. - In the hero carousel the layer reprojects each anchor onto the visible
object-coverpixels (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 (seedocs/I18N.md).
- Every gallery photo on
Fixed
2 entriesChat 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-stickyhiddenflag, 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 isposition: stickyand already occupies that space in flow, so the inset was being counted twice and a fixed5remwas 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 flatmt-2 md:mt-4, its top edge softly tucking into the progressive top-blur band like the full-bleedPageHeros do), and the viewer is taller —aspect-[4/3]on phones (was16/10). ThePhotos / 3Dpill and the overlay chrome drop totop-8 md:top-14so 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 thez-30top-blur overlay. The private-jets detail page had no mobile header at all. Both controls now live insideAircraftHeroas 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.
- The hero's top margin was
Changed
4 entriesSplash-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-materialenvMapIntensitydoes the final dial-in. A hot-swapped/models/jet.glbinherits the same environment and getsenvMapIntensitybumped on load. - Bodywork upgraded from
MeshStandardMaterialto `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.mdupdated with the full materials/lighting rundown.
- 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 (
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 internalLinkmagnets and externaltel:/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-chipwith a black/white (var(--color-fg)onvar(--color-bg)) icon disc; the greenemerald-400presence dot — on both the launcher and the open panel's header — is nowred-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
mdbreakpoint (768 px), where each column collapsed to ~40 px and route names, patient profiles and ETAs wrapped or overflowed. The table grid now activates atlg(≥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 areshrink-0and text spansmin-w-0so 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-6→ms-6) for an RTL-safe logical one.
- Live-feed table — the 12-column grid layout used to switch on at the
Fixed
1 entryAircraft 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 isposition: stickyand already occupies that space in flow, so the inset was being counted twice and a fixed5remwas piled on top. The result was a ~135 px empty band between the navbar and the photo on mobile. Replaced with a flatmt-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 thez-30top-blur overlay. The private-jets detail page had no mobile header at all. Both controls now live insideAircraftHeroas 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.
- The hero's top margin was
Security
1 entryHTTP security headers
(
next.config.ts). Every route now ships with a full set of defensive HTTP headers:Content-Security-Policy—default-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-yearmax-age,includeSubDomains,preload(HSTS preload-list eligible).X-Frame-Options: SAMEORIGIN— clickjacking protection for browsers that do not processframe-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 entriesCustom 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.digestis shown as a reference code when present to assist support triage.
Added
1 entrySkip-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). Addedid="main-content"to the<main>element across 15 public pages and route-group layouts.
Fixed
3 entries3-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 ontext-whiteglyphs over backgrounds that turn light underprefers-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'sbg-black/95backdrop; - 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.- the fullscreen-overlay close (✕) button and the "Drag to orbit · Pinch to zoom" hint pill used
Chat widget dialog — focus management
(
src/components/triforce/chat-bot.tsx). The "Ask Tri" chat panel hadrole="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"andtabIndex={-1}to the dialog container. - Attaching
dialogRefand moving focus to it on open (dialog.focus()). - Attaching
triggerRefto 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>witharia-relevant="additions", so only new chat messages are announced.
- Adding
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 noaria-hidden, causing screen readers to announce the duplicate string. Addedaria-hidden="true"to the containing element.
Added
3 entriesFaqAccordionshared 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-glasscontainer with a rotating.liquid-gradientaccent in the corner, CSSgrid-template-rowsanimation (no JS height measurement, GPU-composited),prefers-reduced-motionrespected viamotion-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-visiblering in the accent colour. Optionally emits an inlineFAQPageJSON-LD<script>for Google rich-result eligibility. Props:items,title,eyebrow,disclaimer,withJsonLd,className.FAQ sections on
/air-ambulanceand/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
FAQPageemitted on each page, making all four pages eligible for Google FAQ rich results.Route-detail FAQ upgraded to
FaqAccordionThe 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 throughFaqAccordion(JSON-LD continues to be emitted separately by the existingfaqLdscript —withJsonLd={false}on the component avoids double-emission). Visual language is now consistent across all FAQ surfaces.
Changed
2 entriesInstall 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 throughgetTranslator(detectLocale(usePathname()))against newchat.*/install.*keys insrc/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 indocs/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.mdnow 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 throughgetTranslator/localizePath, registered inAR_BUILT_ROUTES, RTL-safe, and hreflang-tagged. Points atdocs/I18N.mdfor the mechanics.
Changed
2 entriesTestimonial cards upgraded to Liquid Glass treatment
Cards on
/air-ambulanceand/private-jetsnow use.liquid-glass backdrop-blur-xlinstead of a flatbg-[var(--color-bg-card)]fill, gaining the frosted-glass depth of the rest of the design language. A.liquid-gradientaccent blob sits in the top-right corner of each card (same pattern as the "Beyond the flight" section). TheQuotesicon 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 fromtext-[var(--color-fg-muted)]totext-[var(--color-fg)]/85— the testimonial is the primary content, not a caption. The“”HTML entities are removed (the visual icon already signals a quotation). Institution badges switch from flatrounded-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 entriesSitemap 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/sitemapitself. 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
IntersectionObserveragainst 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
3Dmode onmd+, 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). Theqrcodepackage 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/charterand/air-ambulance/transport.- Real data, not template-fill.
src/lib/routes.tscarries 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
BreadcrumbListandFAQPageJSON-LD (4 route-specific Q&As generated from the route's own data — flight time, cost, aircraft, airports); index pages emitItemList. Eligible for FAQ rich results in search. - Internal linking. Hub pages (
/private-jets,/air-ambulance, and their/arcounterparts) 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/charterwith?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/charterand/ar/air-ambulance/transport— middleware's untranslated-route fallback now exempts those prefixes so they render rather than 307-redirecting to English.docs/I18N.mdanddocs/SEO.mdupdated.
- Real data, not template-fill.
Changed
3 entriesFleet 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 atbottom-3 left-3insrc/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 deprecatedAircraftGalleryandJet3DPreviewcomponents 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 sharedSiteHeader/SiteFooterlocalize 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.tsnow keeps anAR_BUILT_ROUTESallowlist (/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]andair-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 inlocalStorage(keytriforce.favorites.aircraft), broadcasts atriforce:favorites-changedevent 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. UsesuseSyncExternalStoreso 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-inpututility used by the trip builder and air-ambulance intake already enforced this. Documented as a workspace-wide rule inCLAUDE.mdso 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 aposition: fixedlightbox 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>viareact-dom/createPortal, bumps its z-index toz-[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 opaquebg-blackso nothing bleeds through, and additionally callsElement.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 supportrequestFullscreenon non-video elements, silently falls back to the CSS overlay. Pressing Esc to leave native fullscreen also dismisses the dialog. Seesrc/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 itslang/dirattributes) when navigating between routes that share the root layout. So/→/arsetdir="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'shtml[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 intodocument.documentElement.lang,.dir, and.dataset.localeon every route change.Chat bubble "tail" corner now mirrors in Arabic / RTL
The chat-bot bubbles in
src/components/triforce/chat-bot.tsxused physicalrounded-bl-md(bot, typing, magnet) androunded-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-mdfor the start-aligned bot/typing/magnet bubbles,rounded-ee-mdfor the end-aligned user bubble), and replacedtext-lefton the magnet bubble withtext-start. The tail now hugs the speaker on both LTR and RTL layouts.Arabic aircraft cards no longer 404
The
/arlanding pages and section pages link aircraft cards to/ar/<vertical>/fleet/<slug>, but those routes weren't built — aircraft data insrc/lib/aircraft.tsis 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 previewA 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/morepage, and added tositemap.xmlso crawlers index it. Files:src/app/sitemap/page.tsx,src/components/triforce/sitemap-browser.tsx. Coexists with the existingsitemap.tsmetadata file — that one still produces/sitemap.xmlfor search engines; this new route lives at/sitemapfor humans.Quick-reply chips in the flight-desk chat now carry a duotone phosphor glyph
Every option in
src/lib/chat-flows.tsgot an optionaliconfield (OptionIconunion), and the chip renderer insrc/components/triforce/chat-bot.tsxnow prefixes the label with the matching@phosphor-icons/reactduotone glyph ath-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 entriesInstall 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 returnsnullon 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 fromtext-[11px]totext-[9px], tightened tracking (0.18em→0.14em) and padding (px-4 py-1.5→px-3 py-1), and addedwhitespace-nowrapso 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 singlelg+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 becausejustify-betweenrespectsdir. Seedocs/NAVBAR.mdfor the updated breakpoint table.
Added
2 entriesTestimonials 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. ATestimonialscomponent (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
Quotesicon invar(--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-verticalpropagated from the section sovar(--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
/arURL 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 anx-pathnameheader 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.languageswires uphreflangfor English ↔ Arabic on every page that has a translated counterpart, includingx-defaultpointing 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:localeswitches toar_SAon Arabic pages (withen_USasog: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 entriesJet-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-chipwithtext-white. In light mode that chip flips to a 55–95% white frosted background, so the white glyphs effectively vanished. Added aliquid-glass-chip-mediavariant that stays dark-frosted in *both* color schemes (the chips sit on top of media — photos or thebg-black/92lightbox — 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-accentwith 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-overlaypreviously 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 entriesAir-ambulance intake is real now — the "Coming soon" placeholder is gone
/request/air-ambulancepreviously 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-verticalhandoff), 16px-min inputs (no iOS focus zoom),aria-pressedtoggles, 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.textnow 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-ambulancereceives the intake and forwards it to the on-call medical director via Resend when
RESEND_API_KEYis set; otherwise logs the rendered text to stdout (same console-fallback shape as/api/request/charterand 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 isactive_emergency; the HTML version paints a red banner above the table, surfaces equipment needs as red-tinted chips, setsreply_toto 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 isdispatch@triforce.flightsby default; override viaAMBULANCE_REQUEST_TO. Server validates required fields, email shape, and the consent flag (rejects withconsent_requiredotherwise).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, andch.formmagnets all routed to/request/charter, but that page rendered aComing soon: Trip builder with airport autocomplete, aircraft preference, and Stripe-hosted depositdashed-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.formnow 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,
noValidateso our copy-friendly inline validation runs instead of browser defaults.
POST /api/request/charterreceives the submission and forwards it to the concierge inbox via Resend when
RESEND_API_KEYis 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, andreply_tois set to the requester's email so concierge can hit Reply directly. Recipient isdispatch@triforce.flightsby default; override viaCHARTER_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 onmd+because desktop already has the inline viewer in the same viewport. Auto-hides if the per-aircraft.glbisn't on disk yet, so we never invite the user into a missing-asset state.
Changed
3 entries3D-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-bgpanel, 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-gridutility — a 32px grid in--color-borderover--color-bgwith 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 singlelinear-gradientveil that capped atcolor-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-bgat 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-bgtoken (#07090cdark,#f5f6f8light), it matches both modes without media queries.
Fixed
5 entriesGallery image actually paints —
ZoomableImageno longer collapses to 0×0The 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 bothrelative(default class) andabsolute inset-0(from the consumer'scontainerClassName) at the same time. Tailwind v4's compiled CSS emits.relativeAFTER.absolute, so under cascade rules.relativewon → the div wasposition: relativewithinset: 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. Removingrelativefrom the default className lets the consumer'sabsolute inset-0cleanly position the wrapper inside the gallery'saspect-[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). TheZoomableImagedefensive fallback added in PR #54 — wired against a future Unsplash deletion — would flip to a solidbg-[var(--color-bg-card)]placeholder (white in light mode) on any<Image>onErrorevent, and the failed-state was keyed onsrcso 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 firingonErroronce would leave the gallery permanently white. Removed thefailedSrcstate,ImageFallbackcomponent, and the lightboxfailedbranch —<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()insrc/lib/aircraft.tswas unconditionally returningposter: "/models/aircraft/{slug}/poster.webp", but no aircraft ships that file.Jet3DPreviewthen resolvedposterUrl = model.poster ?? poster, so the always-setmodel.posterwon and the reala.herophoto (which exists on disk) was never reached. The next/image optimizer returned400for the missing file. Listing-page cards rendered fine because they reada.herodirectly. Dropped the defaultposterfromdefaultModel3d()— 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 setmodel3d.posterexplicitly.Hero title fits in two lines on every phone width
The home-page hero (
src/app/page.tsx) sized its<h1>attext-[2.5rem](40px) on mobile, and the sharedPageHero(src/components/triforce/page-hero.tsx) attext-[2.25rem](36px). Combined withfont-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 sharedPageHero), so the headline holds its two-line silhouette down to iPhone SE width while the existingsm:text-6xl md:text-7xlsteps 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.tsxwas styledbg-accent/90, but this Tailwind v4 setup never registers--color-accentas a theme token (only--color-aa/--color-pjplus a hand-rolled.bg-accentutility), so the/90opacity 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 tobg-[var(--accent-strong,var(--color-aa-strong))]so the bubble always picks up the darker accent variant (#b8121fambulance vertical /#a4823fcharter vertical), giving white text AA-passing contrast on both light and dark themes.
Changed
3 entriesHamburger drawer now reads as the same Liquid Glass as the navbar pill
The mobile menu in
src/components/triforce/site-header.tsxpreviously put the.liquid-glassbackground and thebackdrop-blur-2xlfilter 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: anisolatewrapper with a dedicated absolutely-positioned.liquid-glasschrome layer (carrying thebackdrop-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 usedtext-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/downloaddevice 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 aGlobeHemisphereWest(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 to22remonsm), 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 aweb-fallbackvariant so users on Linux / Firefox / unknown browsers still see a relevant "Setup guide → /download" hand-off instead of a silent disappearance.
Fixed
4 entriesMagic-link sign-in now tells the operator what's actually broken
Previously every server-side failure — missing
RESEND_API_KEY, missingAUTH_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.tsnow throws a typedMagicLinkSendErrorwith a specificreason(resend_not_configured,resend_rejected,send_failed), and/api/auth/request(src/app/api/auth/request/route.ts) catchesAUTH_SECRETmisconfiguration via the same path and returnsauth_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-inAUTH_LOG_LINK_FALLBACK=1env 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 indocs/AUTH.mdupdated with the new env table and the failure-reason matrix.Production deployments no longer fail at
npm installEvery 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.jsonpinnedthree@^0.184.0and@types/three@^0.184.1while@google/model-viewer@4.2.0declares a strictpeer three@^0.182.0. npm 10 rejects the conflict by default. Pinned boththreeand@types/threeback to^0.182.0to satisfy the model-viewer peer cleanly (three is only used as atypeimport insplash-screen.tsxandsplash-jet.ts, so the minor downgrade has no runtime impact). Verified locally:npm installsucceeds andnext buildcompletes with every fleet detail page in the prerender manifest.Magic-link sign-in no longer reports false success
Previously, when
RESEND_API_KEYwas missing on a deployment the/api/auth/requesthandler 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 real502 send_failedto the user. In dev/preview the route still falls back to the console log but the response now carriesprovider: "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/requestwas building the verify URL fromreq.nextUrl.origin, which on a preview deployment would emit a link back to the*.vercel.apphost — meaning the session cookie would set on the preview domain, not ontriforce.flights, and the user would land on prod still signed-out. The route now readsNEXT_PUBLIC_SITE_URL(already used elsewhere viasrc/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 entriesReal 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 thedangerouslyAllowSVG/CSP flags they required innext.config.tsare gone.Live visitor map on admin overview
New
Live Visitorscard on/adminrenders 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 showsPowered by Triforcealongside the required OpenStreetMap and CARTO credits. Mock data lives insrc/lib/admin-data.ts(VISITORS); production swap is a 10-second poll of/api/admin/visitorsbacked by edge analytics → Postgres. Honorsprefers-reduced-motion; map theme tuned to the admin dark palette via scoped<style>./iosApp Store screenshot galleryNew page at
src/app/ios/page.tsxthat 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 viaaspect-ratioCSS so any zoom-level screenshot still passes App Store Connect's upload checks. Linked off/docsand reachable directly from/ios.docs/CREDITS.md+ pre-wired CC-BY 4.0 model picks for every jetCatalogue-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.creditis pre-filled insrc/lib/aircraft.tsso the moment the GLB file gets dropped atpublic/models/aircraft/<slug>/model.glbthe viewer renders the attribution under the canvas automatically — no second edit pass. TheJet3DPreviewcredit footer now gates onglbStatus === "ready"so the credit doesn't render under the empty-state placeholder.docs/CREDITS.mddocuments 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 aJet3DPreviewsection (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.usdzfile alongside the.glb(Apple's Quick Look will not accept GLB). The viewer HEAD-checks/models/aircraft/<slug>/model.glbon 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 underprefers-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 optionalmodel3dfield ({ glb?, usdz?, poster?, credit? }) and aresolveModel3d()helper insrc/lib/aircraft.tsthat 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+ rewrittenpublic/models/README.mdcovering 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-viewerdependencyInstalled with
--legacy-peer-depsbecause model-viewer's peer range pinsthree@^0.182while the splash screen runsthree@^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 entriesDesktop navbar moved up
Bumped the
lg+stickytopfrom1.25rem(20px) back down to0.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+stickytopflush to the viewport edge atscrollY === 0on 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 bumpedlg+from 0.75rem to 1.25rem so the pill clears the hero with more breathing room thansm/md. Removed the now-unnecessarytransition-[top]since the offset no longer changes between scroll states.docs/NAVBAR.mdupdated.
Changed
3 entriesHero overlay now adds a heavy white veil so headlines pop
Every image-backed hero (home page + every
PageHeroconsumer:/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-overlayutility inglobals.css./docspage redesignThe documentation page was rendering the entire
CHANGELOG.mdas 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 standardPageHero(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.
SiteHeadernow overridestoptomax(0px, env(safe-area-inset-top))atlg+while!islanded, and animates the position withtransition-[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 indocs/NAVBAR.md.
Added
7 entriesLight-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 onprefers-color-scheme: lightthe bottom of the mark became a heavy black wedge against a white surface — both inline (inSiteHeader/SiteFooter/wordmark) and in the browser-tab favicon. Each affected SVG (/src/app/icon.svg,/public/triforce-mark.svg, and the inlineTriforceMarkReact component insrc/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 existinguseId()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
pointermoveon 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. Honorsprefers-reduced-motionand exits cleanly into the existing fly-away animation. Lives insrc/components/triforce/splash-screen.tsx./servicespage — the missing aftermarket / advisory practiceRestores 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.jsonversion, uncommitted changes, last 10 commits, open PRs (viaghif installed), and the top of the## [Unreleased]block from this file. The same snapshot is written todocs/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 ifghis missing.
Fixed
7 entriesLight-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-whiteforced 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-overlayutility keeps the original black gradient in dark mode but flips to acolor-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-bottomutility (five stackedbackdrop-filterlayers, 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 toPageHero(/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. Thetone="onDark"prop onTriforceWordmarkandTriforceLogoStackedis 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 formbg-[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 thebackground-imagerule never actually emitted; the cards fell through to the page bg, which is dark in dark mode (the bug stayed hidden) but#f5f6f8in light mode — leaving everytext-whiteglyph inside the mock-up invisible. The other social cards (InstagramStory,FacebookPost,FacebookLinkShare,TwitterPost,LinkedInPost,QuoteCard) end their stack with alinear-gradient(...)instead and parse fine, which is why only those four needed fixing. Moved the broken four off Tailwind and onto plain inlinestyle={{ background: "..." }}strings (also passedtone="onDark"to theTriforceWordmarkinstances 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 wrotetrack.scrollLeftand then immediately readfirstHalf.scrollWidthinsidenormalize(), forcing a synchronous layout pass. On a high-poll mouse (500–1000 Hz) the cost piled up well past one frame, and thescrollhandler it triggered rannormalize()again. Two changes insrc/components/triforce/jet-marquee.tsx: (1) the half-loop width is now cached and kept fresh with aResizeObserveron the duplicated track, sonormalize()is pure arithmetic — no layout read on the input path; (2)pointermoveno longer writesscrollLeftdirectly — it stashes apendingDragXthat the existing rAF tick applies once per frame (with a flush onpointerup). Thescrollhandler 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-draggingis resolved.Aircraft hero/gallery imagery is now first-party
Replaced every
images.unsplash.comSTOCK constant insrc/lib/aircraft.tswith brand-aligned SVG illustrations shipped underpublic/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.tsopted intodangerouslyAllowSVGwith a strictscript-src 'none'; sandboxCSP so the local SVGs flow throughnext/imagesafely.toSocialImagenow 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 elseZoomableImageis used.ZoomableImagenow wiresonErroron both thenext/imagethumbnail 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 bysrcso 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 allowlistedimages.unsplash.comhost).News article share button now works
The icon on
/news/[slug]was a dead<button>with noonClick. Replaced it with a newShareButtonclient 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 amailto: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 entryImage 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 agroup-hover:scale-[1.03]/transition-transform duration-500effect 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 entriesHero 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 sharedPageHero(/missions,/about,/care-team,/air-ambulance,/private-jets) and the home-page hero frompt-14 sm:pt-20 md:pt-28/pt-12 md:pt-24to a unifiedpt-24 sm:pt-32 md:pt-40so 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-heroutility: SF Pro Display on Apple devices (via the-apple-system/BlinkMacSystemFontaliases) 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 offont-display text-4xl/5xl/6xlrecipes 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 theComingSoonshell. 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, respectsprefers-reduced-motion, pauses on tab hidden) was previously only used on/. It now ships on/air-ambulance,/private-jets,/missions,/about, and/care-teamvia a sharedPageHerocomponent (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 bespokefont-displayheroes on/missions,/about,/care-teamwere also using-mt-10/-mt-14to 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-teamdoesn't wrap into a pencil-thin column on tablet, while the center-aligned home hero keeps its tightermax-w-md md:max-w-lg. Hero h1 size also drops to2.25remon the smallest screens to give the gradient-accent line room to breathe under 360px wide.
Added
14 entriesApple 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 viaPOST /api/auth/request, exchanged atGET /api/auth/verify, sessions held in a single signedtriforce_sessioncookie (HttpOnly,SameSite=Lax,Securein prod, 30-day TTL). Email delivery uses Resend whenRESEND_API_KEYis 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 fromADMIN_EMAILSand 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 §14 *Apple App Store additional terms* block Apple expects (Apple-not-a-party, third-party-beneficiary, etc.). - Medical & 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 “App Privacy” nutrition-label values and the App Review notes to paste into the submission. - Nav surfacing. Site footer gains an *Account & Legal* column (Sign in, Account, Privacy, Terms, Medical & 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.tsdisallows/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.
- Magic-link auth (
"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
fallbackstep that triages back into the four lanes. State persists tosessionStorageso the conversation survives page navigation, and the launcher respects the install-prompt's airspace (delayed appearance, dismissible per session). Flow data lives insrc/lib/chat-flows.ts; UI insrc/components/triforce/chat-bot.tsx; mounted globally fromsrc/app/layout.tsx. Seedocs/CHATBOT.mdfor 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
scrollLeftdirectly — 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 withscrollLeftwrapped into[0, halfWidth)each frame. Edge fade masks hide the wrap point,prefers-reduced-motionfully disables the auto-drift (cards stay scrollable),visibilitychangepauses while the tab is hidden, and a Liquid-GlassPause/Playchip 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-trackstyles added toglobals.css. Wired intosrc/app/page.tsx.System-driven light mode
The site now follows the operating system's
prefers-color-schemepreference. 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 insrc/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 setscolor-scheme: lightso native form controls and scrollbars follow. - The brand accents (
--color-aared and--color-pjgold) 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.themeColorinsrc/app/layout.tsxnow serves#f5f6f8to 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
#ffftovar(--color-fg)so it reads as dark text against a light splash.
- The dark palette stays the default in
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 newtoSocialImagehelper), marketing pages use vertical-appropriate imagery. The root layout now declaresmetadataBase(so all relative OG image URLs resolve to absolute), full defaultopenGraph/twitterblocks, akeywordslist,robots/googleBotdirectives (withmax-image-preview: large), andformatDetection. 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-systemare nowrobots: noindex,nofollow, androbots.txtdisallows/admin/and/offlinein addition to the existing/request/and/api/exclusions.Full build-out of
/about,/care-team, and/missionsThe three pages were placeholder
ComingSoonstubs 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'sliquid-gradientaccent), 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 fromlib/news.ts.
Sitemap refresh
src/app/sitemap.tsnow reflects the new page weights:/missionsbumped to daily / 0.85,/aboutto 0.8,/care-teamto 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 respectsprefers-reduced-motionvia the existing keyframe gate. Implementation: newsrc/components/triforce/zoomable-image.tsx(one client component that owns both the loupe and the lightbox), wired intoaircraft-gallery.tsxso the prev/next carousel keeps working with the new gesture surface. Afade-inkeyframe was added toglobals.cssfor 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-fgon hover via agroupclass 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
RoomEnvironmentis set asscene.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.glbBefore the procedural build runs, the splash HEAD-checks
public/models/jet.glband, if present, loads it viaGLTFLoader, 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. Seepublic/models/README.mdfor the license + size budget rules. Failure is silent — the procedural jet is the always-on fallback. Build code lives insrc/components/triforce/splash-jet.ts.Design system page at
/design-systemA 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
/contactA new "Download Triforce vCard" card on the contact page links to a static
/api/vcardroute that emits a RFC 2426 vCard 3.0 withContent-Type: text/vcardandContent-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 canonicalhttps://triforce.flightsURL, organization, title, and the "staffed every minute of the year" note. Contact metadata is now centralized insrc/lib/contact.tsso the page UI and the vCard payload share a single source of truth and cannot drift.
Changed
1 entryFleet search bar sticks to the viewport on mobile
On
/private-jets/fleetand/air-ambulance/fleet, the search input + filter button row inFleetExplorer(src/components/triforce/fleet-explorer.tsx) now usesposition: stickybelow 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 entriesAircraft 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) toAircraftGallery, but the stuckSiteHeaderpill on an iPhone PWA actually occupiesenv(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 withmt-[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 fromtop-20totop-[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 withsticky bottom-0but had noenv(safe-area-inset-bottom)padding, so on iPhone PWAs / Safari the labels and icons sat under the home indicator and read as half-cut. Addedpb-[env(safe-area-inset-bottom)]to the nav — viewport is alreadyviewportFit: "cover"insrc/app/layout.tsx, and this matches the safe-area pattern already used byinstall-prompt.tsxand the splash screen rules inglobals.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—.exampleis RFC 2606 reserved and would never deliver mail. All public-facing copy now matches the canonical address already used bysrc/app/admin/settings/page.tsxand the staff records insrc/lib/admin-data.ts:dispatch@triforce.flightsandpress@triforce.flights. The fallback insrc/lib/site.ts(NEXT_PUBLIC_SITE_URLdefault) was bumped fromhttps://triforce.exampletohttps://triforce.flightsfor the same reason — production deploys override via env var, but the fallback now resolves rather than 404s.
Added
4 entriesProgressive 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 stackedbackdrop-filterlayers (blur 0.5 → 1.5 → 4 → 10 → 20 px, the last withsaturate(140%)), each masked with a cascadinglinear-gradientso 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 hardbackdrop-blurcut. The band height tracksenv(safe-area-inset-top)plus 96 px on mobile / 128 px on≥md, sits atz-30(below the sticky<SiteHeader>atz-40, above page content), and ispointer-events: noneso it never traps clicks. Honorsprefers-reduced-motionby collapsing to a single 6 px blur with a simple top-to-transparent mask. Styles live as the.progressive-blur-toputility inglobals.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. Honorsprefers-reduced-motion(no canvas, snap fade), pauses ondocument.visibilitychange(perCLAUDE.md), disposes every geometry, material, and renderer on unmount, and falls back gracefully via<noscript>so JS-disabled visitors aren't trapped behind it. Seedocs/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-contentwrapper around{children}is hidden under a 28 px blur withpointer-events: noneandbody { overflow: hidden }. When the splash flips the attribute todata-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 oftransition-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-accentutility 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 withvar(--accent)so the hue still tracks the active vertical (red for ambulance, gold for charter).
Changed
2 entriesNavbar buttons (hamburger + 24/7 Emergency CTA) swapped from
rounded-fullpills to concentricrounded-xl md:rounded-2xl(12px / 16px) so their corner radii follow the parent navbar'srounded-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. Respectsprefers-reduced-motion; per-element opt-out via.no-press.
Changed
6 entriesHome hero is now tighter on phones: the headline drops from
text-5xl(48px) totext-[2.25rem](36px) below thesmbreakpoint and the sub-copy steps down fromtext-lgtotext-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-7toh-9 w-9so the icon visually spans both lines of text, and tightened the gap between "TRIFORCE" and the "AIR AMBULANCE" / "PRIVATE JETS" tagline frommt-1tomt-0.5for 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
useIdgradient ids so multiple logos can render on the same page without collisions.SiteHeadernow centers the wordmark in a 3-column grid: hamburger / left nav links on the left, centeredTriforceWordmark, 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
TriforceLogoStackedcomponent (mark above, "TRIFORCE" wordmark, "AIR AMBULANCE" tagline) and centered the headline + CTAs beneath it for a brand-forward landing. Composed with the existingios-reveal-*entrance animations.Top navbar now only "islands up" once the page has scrolled. At
scrollY === 0the 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-motionskips the transition.
Removed
1 entryTop-navbar WebGL metaball orbs (
NavbarOrbs) and thethree/@types/threedependencies 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 entriesSitemap no longer lists
/request/air-ambulanceor/request/charter.robots.txtalready disallows the/request/prefix, so including those URLs in the sitemap sent conflicting crawl signals (flagged on PR #11).Restored the
lucide-reactdependency. The admin console (PR #4) importsArrowUpRight/ArrowDownRightfromlucide-react, but PR #5 had removed the package — leavingmainunable to build. Re-adding it unblocks the deploy. Follow-up: convert the admin surface to Phosphor duotone icons to match the marketing site, then removelucide-reactagain.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 amargin-topthat pre-positions the header just below the island plus a matching stickytop, 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.16emletter-spacing that was widening every cell, shortened "Care Team" to "Care" (single-word labels match standard iOS/Material tab-bar convention), and addedwhitespace-nowrapas a safety net.
Added
21 entries/icon.svg(Next.js App Router favicon) and/public/triforce-mark.svgusing the same Penrose mark, wired up viametadata.iconsinapp/layout.tsx. Oldfavicon.icoremoved.TriforceLogoStackedsize variants (md/lg/xl) for centered hero/marketing use. The same Penrose mark also replaces the placeholderpublic/icons/icon.svg+icon-maskable.svgused 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 thetheme_colormatches 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/offlineso 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 anavigator.share()shortcut + caret pointing at the share affordance; Android/desktop Chromium gets a one-tap "Install app" backed by the capturedbeforeinstallpromptevent. Includes a "Don't show this again" action that writestriforce.installPrompt.dismissed=forevertolocalStorage, and a soft dismiss that snoozes for 7 days./downloadpagewith 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 +
/morepage now link to Download App.Newsroom:
/newsindex and statically-generated/news/[slug]detail pages with three launch articles (press release, mission report, fleet update). Each article ships withNewsArticleJSON-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 inpublic/llms.txtand the XML sitemap.app/sitemap.ts(Next.js Metadata Routes) generating an XML sitemap at/sitemap.xmlfor all marketing pages, news articles, and fleet detail pages, with per-sectionchangeFrequencyandpriority.app/robots.tsgenerating/robots.txtthat allows crawling, blocks/api/,/_next/, and the/request/flows, and points search engines to the sitemap.SITE_URLhelper insrc/lib/site.ts, defaulting tohttps://triforce.example, overridable viaNEXT_PUBLIC_SITE_URL.iOS-style scroll storytelling on the home page. A new
ScrollStorycomponent 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.StatRevealstrip with rAF count-up numbers (ARGUS / IS-BAO / 2,400 missions / 187 countries) triggered by IntersectionObserver.StackingCardsfinale: 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 nativeanimation-timeline: view()/scroll()where supported and falls back to an IntersectionObserver-driven.is-inclass viaRevealOnScroll. All animations respectprefers-reduced-motion.public/llms.txtso 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-glassand.liquid-gradientutilities +liquid-spinkeyframes inglobals.css.
Changed
3 entriesHero typography now leans on a brighter, bolder treatment so the display copy holds the dark hero photo. Title scale bumped (
text-5xl → text-7xlon desktop) and weight raised tofont-semibold/font-boldon the accent line. The accent spans on the home hero ("Anytime. Anywhere.") and closing line ("We do.") now use a new.text-gradient-accentutility that lifts the top of each glyph toward an accent‑white mix — the raw#e11d2e/#c8a464accents were reading as muddy against--color-bg. Body paragraph under the hero is nowfont-mediumattext-lg/text-xlon 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 newHeroParallaxclient component runs arequestAnimationFrameloop 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 underprefers-reduced-motion.Top navbar now only "islands up" once the page has scrolled. At
scrollY === 0the 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-motionskips the transition.
Removed
1 entryTop-navbar WebGL metaball orbs (
NavbarOrbs) and thethree/@types/threedependencies 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 entriesInstall banner now surfaces a macOS Safari variant pointing at
File → Add to Dock…. Safari 17+ supports installable web apps, but it never firesbeforeinstallprompt, 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-ambulanceor/request/charter.robots.txtalready disallows the/request/prefix, so including those URLs in the sitemap sent conflicting crawl signals (flagged on PR #11).Restored the
lucide-reactdependency. The admin console (PR #4) importsArrowUpRight/ArrowDownRightfromlucide-react, but PR #5 had removed the package — leavingmainunable to build. Re-adding it unblocks the deploy. Follow-up: convert the admin surface to Phosphor duotone icons to match the marketing site, then removelucide-reactagain.StatRevealreduced-motion early-exit no longer trips React 19'sreact-hooks/set-state-in-effectrule — the synchronoussetValue(target)is now scheduled inside a one-shotrequestAnimationFrame.Mobile bottom-nav labels no longer wrap to two lines on narrow phones. Removed the
uppercase+0.16emletter-spacing that was widening every cell, shortened "Care Team" to "Care" (single-word labels match standard iOS/Material tab-bar convention), and addedwhitespace-nowrapas a safety net.