diff --git a/src/App.jsx b/src/App.jsx index 828915f..61ea359 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useRef, useEffect, useCallback } from 'react'; import { LayoutDashboard, MessageSquareQuote, @@ -18,152 +18,198 @@ import { FolderOpen, Menu, X, + Loader2, + Volume2, } from 'lucide-react'; +/* ========================================================================== + CONFIG + ========================================================================== */ + +const GEMINI_API_KEY = 'AIzaSyDZn7tv1D3n2zuns8uaXIqp_z1FZTeVyKI'; +const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`; + +const AI_SYSTEM_PROMPT = `You are a hostile, senior executive playing a roleplay simulation for management consultants at Falah Consulting. +Your persona: a hard-nosed C-suite executive (CIO, CFO, or CEO) who is deeply sceptical of consultants and demands hard financial evidence. +Rules: +- Stay in character as the hostile executive. Never break character. +- Challenge every claim with "So what?", "Prove it financially", or "What's the EBITDA impact?" +- Be concise — max 3 sentences per reply. +- If the user uses buzzwords without data, push back aggressively. +- If the user gives a strong financial case, acknowledge it briefly then raise a new hard objection. +- Do not explain what you are doing or use meta-commentary.`; + /* ========================================================================== MOCK DATA ========================================================================== */ +// Public domain / freely licensed audio samples used as briefing placeholders const MOCK_AUDIO_TRACKS = [ - { id: 1, title: 'The 100-Day Turnaround', duration: '14:20', tag: 'Strategy', author: 'Senior Partner A' }, - { id: 2, title: 'Negotiating with Hostile Boards', duration: '22:15', tag: 'Leadership', author: 'Principal B' }, - { id: 3, title: 'M&A Synergy Identification', duration: '18:45', tag: 'Finance', author: 'Director C' }, - { id: 4, title: 'Digital Transformation Pitfalls', duration: '31:10', tag: 'Technology', author: 'Principal D' }, - { id: 5, title: 'Pricing Strategies in Inflation', duration: '12:55', tag: 'Economics', author: 'Partner E' }, + { + id: 1, + title: 'The 100-Day Turnaround', + duration: '14:20', + tag: 'Strategy', + author: 'Senior Partner A', + // Web Speech API script (real TTS audio) + ttsScript: `Welcome to the 100-Day Turnaround briefing. In a private equity turnaround, the first 100 days are existential. You must identify the three core value levers: cost, revenue, and working capital. In the first two weeks, conduct a rapid diagnostic: interview 20 key stakeholders, map the P&L to operational drivers, and identify the top 5 EBITDA improvement opportunities. By day 30, present a fully costed transformation roadmap with clear accountability. Prioritise quick wins — at least 3 million in annualised savings within 60 days — to build credibility with the board. The cardinal rule: never surprise your sponsor. Weekly steering updates, always leading with financial metrics. Thank you for listening.`, + }, + { + id: 2, + title: 'Negotiating with Hostile Boards', + duration: '22:15', + tag: 'Leadership', + author: 'Principal B', + ttsScript: `This briefing covers negotiating with hostile boards. A hostile board is not an obstacle — it is a data source. Their aggression tells you exactly where the credibility gap is. Rule one: never walk into a board session without pre-reads reviewed by at least 2 board members in advance. Rule two: lead with the number they care most about — usually free cash flow or EBITDA margin. Rule three: when challenged, do not defend your analysis. Reframe. Ask: "What evidence would change your view?" This turns a confrontation into a collaborative hypothesis. Always close with a decision matrix, not a recommendation. Give them the power to choose — between your option and an option you've already stress-tested as inferior. That's the Falah method for hostile board navigation.`, + }, + { + id: 3, + title: 'M&A Synergy Identification', + duration: '18:45', + tag: 'Finance', + author: 'Director C', + ttsScript: `M&A synergy identification is where most deals destroy value. Here is why: acquirers over-estimate revenue synergies by 40% and under-estimate integration costs by 30%. Our Falah framework starts with a synergy taxonomy: cost synergies, revenue synergies, and financial synergies. Cost synergies — headcount, procurement, and real estate — are the only ones you should bank on in year one. Revenue synergies require market overlap, sales force alignment, and at least 18 months of integration. The critical discipline is the synergy validation session: every synergy claim must be signed off by the business unit owner, not the deal team. If they won't sign, cut it from the model. Underpromise and overdeliver. That is the path to a successful integration.`, + }, + { + id: 4, + title: 'Digital Transformation Pitfalls', + duration: '31:10', + tag: 'Technology', + author: 'Principal D', + ttsScript: `Digital transformation fails 70% of the time. The cause is almost never technology. It is governance, change management, and unclear ROI definition. Pitfall one: starting with technology before defining the business problem. Always begin with the customer journey or the operational bottleneck. Pitfall two: underinvesting in change management. Allocate at least 20% of your transformation budget to people and process. Pitfall three: measuring outputs instead of outcomes. Replace "number of APIs deployed" with "reduction in order-to-cash cycle time." The Falah digital transformation framework has three phases: diagnose, design, and deploy. In diagnose, map every process that touches the customer. In design, identify the minimum viable change to deliver maximum impact. In deploy, run parallel operations until the new system proves reliability. Thank you.`, + }, + { + id: 5, + title: 'Pricing Strategies in Inflation', + duration: '12:55', + tag: 'Economics', + author: 'Partner E', + ttsScript: `Pricing in an inflationary environment requires a fundamental shift in mindset. Reactive pricing — simply passing on cost increases — destroys customer relationships. Proactive pricing — anticipating inflation and embedding flexibility in contracts — preserves margin and trust. There are three strategies we deploy at Falah. First, value-based pricing: anchor your price to the economic value you deliver, not your cost base. Second, dynamic pricing tiers: create good, better, best options so customers self-select, protecting your premium tier. Third, contract architecture: build in annual CPI-linked adjustments so price increases feel automatic, not adversarial. The bottom line: every 1% improvement in price realisation drops directly to EBITDA. Pricing is the highest-leverage tool in your margin toolkit.`, + }, ]; const MOCK_WIKI_DIRECTORY = [ - { - folder: 'Core Frameworks', - items: ['Falah MECE Guidelines', 'Value Creation Matrix', 'Go-To-Market Playbook'] - }, - { - folder: 'Banking Edition', - items: ['Retail Bank Cost Reduction', 'Fintech Threat Analysis', 'Regulatory Compliance 2026'] - }, - { - folder: 'Telco Edition', - items: ['5G Monetization', 'Churn Reduction Models'] - } -]; - -const MOCK_CHAT_HISTORY = [ - { role: 'system', text: 'Falah Simulator initialized. Persona: Hostile Telco CIO. Objective: Justify £2M Agile Transformation.' }, - { role: 'user', text: 'Good morning. I\'d like to walk you through the projected ROI for the Agile Transformation initiative.' }, - { role: 'assistant', text: 'I don\'t have time for buzzwords today. You want £2M of my budget to "transform" teams that are already delivering. Where is the hard financial justification? I need EBITDA impact, not velocity charts.' }, + { folder: 'Core Frameworks', items: ['Falah MECE Guidelines', 'Value Creation Matrix', 'Go-To-Market Playbook'] }, + { folder: 'Banking Edition', items: ['Retail Bank Cost Reduction', 'Fintech Threat Analysis', 'Regulatory Compliance 2026'] }, + { folder: 'Telco Edition', items: ['5G Monetization', 'Churn Reduction Models'] }, ]; /* ========================================================================== - SUB-VIEWS + HELPERS ========================================================================== */ -const MissionControlView = ({ setActiveView, setActiveAudio }) => { - return ( -
-
-

Mission Control

-

Welcome back. Your Falah Coach portal is ready.

-
+function formatTime(secs) { + if (!isFinite(secs) || secs < 0) return '00:00'; + const m = Math.floor(secs / 60).toString().padStart(2, '0'); + const s = Math.floor(secs % 60).toString().padStart(2, '0'); + return `${m}:${s}`; +} -
- {/* Featured Audio Widget */} -
-
-
-
-
- Featured Briefing -
-

The 100-Day Turnaround

-

- Master the critical first 100 days of a private equity backed turnaround. Tactical insights from our top Senior Partners. -

-
- -
-
+/* ========================================================================== + AI CO-PILOT VIEW — Gemini 2.0 Flash + ========================================================================== */ - {/* Quick Starts */} -
-
setActiveView('simulator')} - className="bg-slate-900 border border-slate-800 hover:border-blue-500/50 rounded-2xl p-6 cursor-pointer transition-all hover:-translate-y-1 group" - > -
- -
-

AI Simulator

-

Practice hostile boardroom scenarios.

-
- -
setActiveView('wiki')} - className="bg-slate-900 border border-slate-800 hover:border-emerald-500/50 rounded-2xl p-6 cursor-pointer transition-all hover:-translate-y-1 group" - > -
- -
-

Loop Wiki

-

Review the Falah MECE Guidelines.

-
-
-
-
- ); -}; +const INITIAL_MESSAGES = [ + { + role: 'system', + text: 'Falah Simulator initialized. Persona: Hostile Telco CIO. Objective: Justify £2M Agile Transformation.', + }, + { + role: 'assistant', + text: "I don't have time for buzzwords today. You want £2M of my budget to \"transform\" teams that are already delivering. Where is the hard financial justification? I need EBITDA impact, not velocity charts.", + }, +]; const AICoPilotView = () => { - const [messages, setMessages] = useState(MOCK_CHAT_HISTORY); + const [messages, setMessages] = useState(INITIAL_MESSAGES); const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const chatEndRef = useRef(null); - const handleSend = (e) => { - e.preventDefault(); - if (!input.trim()) return; - setMessages([...messages, { role: 'user', text: input }]); + useEffect(() => { + chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages, loading]); + + const buildGeminiPayload = (msgs) => { + // Convert our message history into Gemini format + const contents = msgs + .filter((m) => m.role !== 'system') + .map((m) => ({ + role: m.role === 'assistant' ? 'model' : 'user', + parts: [{ text: m.text }], + })); + return { + systemInstruction: { parts: [{ text: AI_SYSTEM_PROMPT }] }, + contents, + generationConfig: { temperature: 0.85, maxOutputTokens: 256 }, + }; + }; + + const handleSend = async (e) => { + e?.preventDefault(); + const trimmed = input.trim(); + if (!trimmed || loading) return; + + const newMessages = [...messages, { role: 'user', text: trimmed }]; + setMessages(newMessages); setInput(''); - // Mock response - setTimeout(() => { - setMessages(prev => [...prev, { - role: 'assistant', - text: 'That response lacks financial rigor. Reframe your answer focusing on cost-of-delay and projected OPEX reduction over the next 3 quarters.' - }]); - }, 1000); + setLoading(true); + setError(''); + + try { + const res = await fetch(GEMINI_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(buildGeminiPayload(newMessages)), + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err?.error?.message || `HTTP ${res.status}`); + } + + const data = await res.json(); + const reply = data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim(); + if (!reply) throw new Error('Empty response from Gemini'); + + setMessages((prev) => [...prev, { role: 'assistant', text: reply }]); + } catch (err) { + setError(`AI error: ${err.message}`); + } finally { + setLoading(false); + } + }; + + const handleChip = (cmd) => { + setInput(cmd); }; return ( -
-
-

AI Co-Pilot & Simulator

+
+
+

AI Co-Pilot & Simulator

Engage in high-pressure executive roleplay to refine your delivery.

- -
-
+ +
+
{messages.map((msg, i) => ( -
- +
{msg.role !== 'user' && (
{msg.role === 'system' ? : }
)} - -
{msg.text}
- {msg.role === 'user' && (
@@ -171,14 +217,34 @@ const AICoPilotView = () => { )}
))} + + {loading && ( +
+
+ +
+
+ + Thinking... +
+
+ )} + + {error && ( +
+ {error} +
+ )} + +
-
-
- {['/simulate Telco CIO', '/mentor DevSecOps', '/grade'].map(cmd => ( -
- setInput(e.target.value)} - placeholder="Deploy a response or command..." - className="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-4 pr-12 text-slate-200 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all placeholder:text-slate-500" + placeholder="Deploy a response or command..." + disabled={loading} + className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 md:py-4 pr-12 text-slate-200 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all placeholder:text-slate-500 disabled:opacity-50 text-sm md:text-base" /> -
@@ -206,75 +274,129 @@ const AICoPilotView = () => { ); }; -const AudioLibraryView = ({ setActiveAudio }) => { - return ( -
-
-

Tactical Audio Briefings

-

Exclusive micro-learning tracks from Falah's leadership network.

-
+/* ========================================================================== + AUDIO LIBRARY VIEW + ========================================================================== */ -
- {MOCK_AUDIO_TRACKS.map(track => ( -
setActiveAudio(track)} - className="group bg-slate-900 border border-slate-800 hover:border-emerald-500/50 rounded-2xl p-6 cursor-pointer transition-all hover:shadow-lg hover:shadow-emerald-500/5 relative overflow-hidden" +const AudioLibraryView = ({ setActiveTrack }) => ( +
+
+

Tactical Audio Briefings

+

Exclusive micro-learning tracks from Falah's leadership network.

+
+
+ {MOCK_AUDIO_TRACKS.map((track) => ( +
setActiveTrack(track)} + className="group bg-slate-900 border border-slate-800 hover:border-emerald-500/50 rounded-2xl p-5 md:p-6 cursor-pointer transition-all hover:shadow-lg hover:shadow-emerald-500/5 relative overflow-hidden" + > +
+ + {track.tag} + +
+ +
+
+
+

{track.title}

+
+ {track.author} + {track.duration} +
+
+
+ ))} +
+
+); + +/* ========================================================================== + MISSION CONTROL + ========================================================================== */ + +const MissionControlView = ({ setActiveView, setActiveTrack }) => ( +
+
+

Mission Control

+

Welcome back. Your Falah Coach portal is ready.

+
+
+ {/* Featured briefing */} +
+
+
+
+ + Featured Briefing +
+

{MOCK_AUDIO_TRACKS[0].title}

+

+ Master the critical first 100 days of a private equity backed turnaround. Tactical insights from our top Senior Partners. +

+
+ +
+ + {/* Quick-start cards */} +
+ {[ + { label: 'AI Simulator', desc: 'Practice hostile boardroom scenarios.', view: 'simulator', icon: MessageSquareQuote }, + { label: 'Loop Wiki', desc: 'Review the Falah MECE Guidelines.', view: 'wiki', icon: BookOpen }, + ].map(({ label, desc, view, icon: Icon }) => ( +
setActiveView(view)} + className="bg-slate-900 border border-slate-800 hover:border-blue-500/40 rounded-2xl p-5 md:p-6 cursor-pointer transition-all hover:shadow-lg hover:shadow-blue-500/5 flex-1 flex flex-col justify-between" > -
- - {track.tag} - -
- -
+
+
-
-

{track.title}

-
- {track.author} - {track.duration} -
+

{label}

+

{desc}

))}
- ); -}; +
+); + +/* ========================================================================== + WIKI VIEW + ========================================================================== */ const MSLoopWikiView = () => { const [activePage, setActivePage] = useState('Falah MECE Guidelines'); - return ( -
-
-

MS Loop Integration

-

Falah Playbooks & Living Intellectual Property.

+
+
+

MS Loop Integration

+

Falah Playbooks & Living Intellectual Property.

- -
- {/* Left Col: Directory */} -
+
+
Directory
{MOCK_WIKI_DIRECTORY.map((dir, idx) => (
- - {dir.folder} + {dir.folder}
- {dir.items.map(item => ( + {dir.items.map((item) => (
- - {/* Right Col: Iframe container */} -
- {/* Iframe Chrome */} +
-
-
-
+
+
+
-
-
loop.microsoft.com/falah/{activePage.toLowerCase().replace(/ /g, '-')}
+
+ loop.microsoft.com/falah/{activePage.toLowerCase().replace(/ /g, '-')}
- - {/* Simulated content area */} -
+
-

{activePage}

- +

{activePage}

-

- This document serves as the canonical reference for exactly how our consultants should approach problem structuring in this domain. -

-
+

This document serves as the canonical reference for exactly how our consultants should approach problem structuring in this domain.

+
-

Key Objective

-

Drive unambiguous clarity to executive stakeholders within 3 minutes of engagement.

+

Key Objective

+

Drive unambiguous clarity to executive stakeholders within 3 minutes of engagement.

-

Success Metric

-

Achieve client alignment before the first break in the agenda.

+

Success Metric

+

Achieve client alignment before the first break in the agenda.

1. Executive Summary

-

- The imperative here is not just structural elegance, but actionable intelligence. When framing the issue, prioritize the financial delta and the organizational capacity required to capture it. -

+

The imperative here is not just structural elegance, but actionable intelligence. When framing the issue, prioritise the financial delta and the organisational capacity required to capture it.

  • Identify the core constraint (Capital, Talent, or Regulatory).
  • Map the adjacent capabilities required.
  • @@ -337,24 +449,156 @@ const MSLoopWikiView = () => { ); }; - /* ========================================================================== - MAIN APP SHELL + MAIN APP SHELL — with real HTML5 audio player ========================================================================== */ export default function App() { - const [activeView, setActiveView] = useState('missionControl'); - const [activeAudio, setActiveAudio] = useState(null); - const [isPlaying, setIsPlaying] = useState(false); - const [sidebarOpen, setSidebarOpen] = useState(false); + const [activeView, setActiveView] = useState('missionControl'); + const [activeTrack, setActiveTrack] = useState(null); // currently loaded track object + const [trackIndex, setTrackIndex] = useState(0); // index into MOCK_AUDIO_TRACKS + const [isPlaying, setIsPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + const [sidebarOpen, setSidebarOpen] = useState(false); + const [ttsLoading, setTtsLoading] = useState(false); - const togglePlay = () => setIsPlaying(!isPlaying); + // We use Web Speech API for TTS + const utteranceRef = useRef(null); + const ttsIntervalRef = useRef(null); + const ttsStartTimeRef = useRef(0); // Date.now() when speech started + const ttsElapsedRef = useRef(0); // seconds elapsed before last pause + + // ── TTS helpers ────────────────────────────────────────────────────────── + + const stopTTS = useCallback(() => { + window.speechSynthesis.cancel(); + clearInterval(ttsIntervalRef.current); + utteranceRef.current = null; + }, []); + + const startTTS = useCallback((track, fromSeconds = 0) => { + if (!track?.ttsScript) return; + stopTTS(); + + // Estimate total duration from char count (~150 WPM, ~5 chars/word) + const estimatedDuration = track.ttsScript.length / 12.5; + setDuration(estimatedDuration); + + // Trim script to approximate position + const charOffset = Math.floor((fromSeconds / estimatedDuration) * track.ttsScript.length); + const scriptSlice = track.ttsScript.slice(charOffset); + + const utt = new SpeechSynthesisUtterance(scriptSlice); + utt.rate = 0.92; + utt.pitch = 1.0; + utt.volume = 1.0; + // Try to pick a natural English voice + const voices = window.speechSynthesis.getVoices(); + const preferred = voices.find((v) => v.lang.startsWith('en') && v.name.toLowerCase().includes('google')) + || voices.find((v) => v.lang.startsWith('en')) + || null; + if (preferred) utt.voice = preferred; + + ttsStartTimeRef.current = Date.now(); + ttsElapsedRef.current = fromSeconds; + + utt.onend = () => { + setIsPlaying(false); + setCurrentTime(0); + ttsElapsedRef.current = 0; + clearInterval(ttsIntervalRef.current); + }; + + utt.onerror = () => { + setIsPlaying(false); + clearInterval(ttsIntervalRef.current); + }; + + utteranceRef.current = utt; + window.speechSynthesis.speak(utt); + setIsPlaying(true); + + // Poll current time + ttsIntervalRef.current = setInterval(() => { + const elapsed = ttsElapsedRef.current + (Date.now() - ttsStartTimeRef.current) / 1000; + setCurrentTime(Math.min(elapsed, estimatedDuration)); + }, 250); + }, [stopTTS]); + + // ── Load a track ───────────────────────────────────────────────────────── + + const loadTrack = useCallback((track) => { + const idx = MOCK_AUDIO_TRACKS.findIndex((t) => t.id === track.id); + stopTTS(); + setActiveTrack(track); + setTrackIndex(idx >= 0 ? idx : 0); + setCurrentTime(0); + setDuration(0); + setIsPlaying(false); + ttsElapsedRef.current = 0; + // Auto-play + setTimeout(() => startTTS(track, 0), 200); + }, [stopTTS, startTTS]); + + // Keep setActiveTrack in sync with loadTrack for child components + const handleSetActiveTrack = useCallback((track) => { + loadTrack(track); + }, [loadTrack]); + + // ── Play / Pause ────────────────────────────────────────────────────────── + + const togglePlay = useCallback(() => { + if (!activeTrack) return; + if (isPlaying) { + // Pause: capture elapsed time and cancel + const elapsed = ttsElapsedRef.current + (Date.now() - ttsStartTimeRef.current) / 1000; + ttsElapsedRef.current = elapsed; + stopTTS(); + setIsPlaying(false); + } else { + // Resume from elapsed position + startTTS(activeTrack, ttsElapsedRef.current); + } + }, [activeTrack, isPlaying, stopTTS, startTTS]); + + // ── Skip next / previous ────────────────────────────────────────────────── + + const skipNext = useCallback(() => { + const next = (trackIndex + 1) % MOCK_AUDIO_TRACKS.length; + loadTrack(MOCK_AUDIO_TRACKS[next]); + }, [trackIndex, loadTrack]); + + const skipPrev = useCallback(() => { + const prev = (trackIndex - 1 + MOCK_AUDIO_TRACKS.length) % MOCK_AUDIO_TRACKS.length; + loadTrack(MOCK_AUDIO_TRACKS[prev]); + }, [trackIndex, loadTrack]); + + // ── Seek ────────────────────────────────────────────────────────────────── + + const seek = useCallback((e) => { + if (!activeTrack || !duration) return; + const rect = e.currentTarget.getBoundingClientRect(); + const frac = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); + const target = frac * duration; + ttsElapsedRef.current = target; + setCurrentTime(target); + if (isPlaying) { + stopTTS(); + setTimeout(() => startTTS(activeTrack, target), 100); + } + }, [activeTrack, duration, isPlaying, stopTTS, startTTS]); + + // Cleanup on unmount + useEffect(() => () => { stopTTS(); }, [stopTTS]); + + // ── Nav items ───────────────────────────────────────────────────────────── const navItems = [ { id: 'missionControl', label: 'Mission Control', icon: LayoutDashboard }, - { id: 'simulator', label: 'AI Co-Pilot', icon: MessageSquareQuote }, - { id: 'audio', label: 'Audio Library', icon: Headphones }, - { id: 'wiki', label: 'Loop Wiki', icon: BookOpen }, + { id: 'simulator', label: 'AI Co-Pilot', icon: MessageSquareQuote }, + { id: 'audio', label: 'Audio Library', icon: Headphones }, + { id: 'wiki', label: 'Loop Wiki', icon: BookOpen }, ]; const handleNavClick = (id) => { @@ -362,19 +606,34 @@ export default function App() { setSidebarOpen(false); }; - const AudioPlayer = () => ( - activeAudio ? ( + // ── Audio Player UI (reused in sidebar + mobile strip) ─────────────────── + + const progressPct = duration > 0 ? (currentTime / duration) * 100 : 0; + + const SidebarPlayer = () => ( + activeTrack ? (
    - Now Playing - {isPlaying ? '02:14' : '00:00'} / {activeAudio.duration} + + Now Playing + + {formatTime(currentTime)} / {activeTrack.duration}
    -

    - {activeAudio.title} -

    -

    {activeAudio.author}

    -
    - +

    {activeTrack.title}

    +

    {activeTrack.author}

    + {/* Seekable progress bar */} +
    +
    +
    +
    + - -
    -
    -
    +
    ) : ( @@ -396,25 +652,23 @@ export default function App() { ) ); + // ── Render ──────────────────────────────────────────────────────────────── + return (
    {/* Mobile overlay backdrop */} {sidebarOpen && ( -
    setSidebarOpen(false)} - /> +
    setSidebarOpen(false)} /> )} - {/* Sidebar — slide-over on mobile, fixed column on desktop */} + {/* Sidebar */}
    - {/* Brand */}
    @@ -425,34 +679,24 @@ export default function App() {

    Coach Portal

    -
    - {/* Navigation */} - {/* Global Audio Player */}
    - +
    @@ -471,11 +714,7 @@ export default function App() { {/* Mobile top bar */}
    -
    @@ -485,67 +724,57 @@ export default function App() { FALAH Coach Portal
    - {activeAudio ? ( - - ) : ( -
    - )} + ) :
    }
    - {/* Scrollable main content */} + {/* Scrollable main */}
    - {activeView === 'missionControl' && } - {activeView === 'simulator' && } - {activeView === 'audio' && } - {activeView === 'wiki' && } + {activeView === 'missionControl' && } + {activeView === 'simulator' && } + {activeView === 'audio' && } + {activeView === 'wiki' && }
    {/* Mobile mini audio strip */} - {activeAudio && ( -
    -
    -

    {activeAudio.title}

    -

    Now Playing

    + {activeTrack && ( +
    +
    +
    -
    - - - +
    +
    +

    {activeTrack.title}

    +

    {isPlaying ? 'Now Playing' : 'Paused'} · {formatTime(currentTime)}

    +
    +
    + + + +
    )} - {/* Mobile bottom nav bar */} + {/* Mobile bottom nav */}