Unreleased
Changed
4 entriesSimulator: 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 hero thumbnail (44–48 px) beside the name, category, and speed / range lines instead of a tall image-first layout, so the strip stays compact on small viewports.
Fixed
5 entriesFleet 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.