From 5ffcead07e9d202dd3108e9a11a95ed43f4f1313 Mon Sep 17 00:00:00 2001 From: Remi <73385395+itsrubberduck@users.noreply.github.com> Date: Sun, 21 Sep 2025 18:22:28 +0200 Subject: [PATCH] Revamp learn flight setup and briefing UX --- app/pages/learn.vue | 1167 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 928 insertions(+), 239 deletions(-) diff --git a/app/pages/learn.vue b/app/pages/learn.vue index df1fe52..35feb57 100644 --- a/app/pages/learn.vue +++ b/app/pages/learn.vue @@ -6,7 +6,7 @@
OpenSquawk | @@ -82,7 +82,7 @@
- mdi-target-account + mdi-target
{{ missionCompletionPct }}%
@@ -121,7 +121,7 @@ >
- mdi-star-four-points + mdi-star New briefing
@@ -197,12 +197,12 @@ type="button" @click="openMissionBriefing()" > - mdi-clipboard-text-search + mdi-magnify Briefing
- mdi-progress-check {{ doneCount(current.id) }}/{{ current.lessons.length }} + mdi-check-circle-outline {{ doneCount(current.id) }}/{{ current.lessons.length }} mdi-star Ø {{ avgScore(current.id) }}%
@@ -249,182 +249,303 @@ Roll another flight
-
-
- mdi-airplane - Flight identity -
-
- - - -
-
+
+
+
+
+
+ mdi-pencil +
+
+
Design your own mission
+

Fill in the essentials below. Expand the optional sections when you want to brief gates, taxi routes or procedures.

+
+
+
-
-
- mdi-airplane-takeoff - Departure -
-
- - - - - - - - -
-
+
+
+
+ mdi-airplane +
+
+
Core flight data
+

We only need a callsign plus departure and destination to spin up a training scenario.

+
+
+
+ + + + + +
+
-
-
- mdi-airplane-landing - Arrival -
-
- - - - - - - - - -
-
+
+ + +
+
+ + + + + + + +
+
+
+
-
-
- mdi-altimeter - Altitudes & codes -
-
- - - - - -
-
+
+ + +
+
+ + + + + + + + +
+
+
+
-
-
- mdi-alert - {{ error }} -
-
+
+ + +
+
+ + + + + +
+
+
+
-
- - +
+
+ mdi-alert + {{ error }} +
+
+ +
+ + +
+
+ +
-
- mdi-clipboard-text-clock +
+
+ mdi-clipboard-text-outline +
-
SimBrief dispatch
+
Import from SimBrief

- Import the latest OFP from - SimBrief. + SimBrief is a free dispatch planner by Navigraph. Build a flight on their + Dispatch page + and we'll pull the latest OFP straight into your mission.

+
    +
  1. + 1 +
    Open the SimBrief dispatch page and generate a flight plan with the aircraft you want to train.
    +
  2. +
  3. + 2 +
    On the resulting OFP, copy your SimBrief Pilot ID (it's the number shown on the share link).
    +
  4. +
  5. + 3 +
    Paste the ID below — we remember it on this device so you can load future plans instantly.
    +
  6. +
+

+ New to SimBrief? It's a full airline-style dispatch tool that pairs perfectly with OpenSquawk missions. +

Contacting SimBrief… @@ -449,7 +573,7 @@ mdi-airplane {{ simbriefPlanMeta.callsign }} · {{ simbriefPlanMeta.route }}
@@ -457,13 +581,13 @@
-
+
Mission hero
Mission briefing
{{ briefingSnapshot.radioCall }}
{{ briefingSnapshot.route }}
-
{{ briefingSnapshot.tagline }}
+

A quick situational overview before you hop back on frequency.

mdi-airplane-takeoff @@ -474,67 +598,137 @@ {{ briefingSnapshot.arrival.icao }} · RWY {{ briefingSnapshot.arrival.runway }} - mdi-form-textbox-password + mdi-shield-airplane Squawk {{ briefingSnapshot.codes.squawk }}
-
-
- Route preview -
- mdi-map-marker-path - Flight deck +
+
+ mdi-airplane +
+ Callsign + {{ briefingSnapshot.callsign }} + {{ briefingSnapshot.radioCall }}
-
    -
  • Push: {{ briefingSnapshot.codes.push }}
  • -
  • ATIS: Information {{ briefingSnapshot.departure.atis }}
  • -
  • Delivery: {{ briefingSnapshot.departure.freq }} · {{ briefingSnapshot.departure.freqWords }}
  • -
-
- Departure -
- mdi-airplane-takeoff - Departure flow +
+ mdi-airplane-takeoff +
+ Departure + {{ briefingSnapshot.departure.icao }} · RWY {{ briefingSnapshot.departure.runway }} + {{ briefingSnapshot.departure.city }}
-
    -
  • Stand: {{ briefingSnapshot.departure.stand || 'As assigned' }}
  • -
  • Taxi: {{ briefingSnapshot.departure.taxiRoute || 'As assigned' }}
  • -
  • SID: {{ briefingSnapshot.departure.sid }} · {{ briefingSnapshot.departure.transition }}
  • -
  • Initial altitude: {{ briefingSnapshot.altitudes.initial }}
  • -
-
- Arrival -
- mdi-airplane-landing - Arrival setup +
+ mdi-airplane-landing +
+ Arrival + {{ briefingSnapshot.arrival.icao }} · RWY {{ briefingSnapshot.arrival.runway }} + {{ briefingSnapshot.arrival.city }}
-
    -
  • STAR: {{ briefingSnapshot.arrival.star }} · {{ briefingSnapshot.arrival.transition }}
  • -
  • Approach: {{ briefingSnapshot.arrival.approach }}
  • -
  • Taxi-in: {{ briefingSnapshot.arrival.taxiRoute || 'As assigned' }}
  • -
  • Arrival stand: {{ briefingSnapshot.arrival.stand || 'As assigned' }}
  • -
-
- Weather -
- mdi-weather-cloudy - Weather +
+ mdi-radar +
+ Codes + Squawk {{ briefingSnapshot.codes.squawk }} + Push {{ briefingSnapshot.codes.push }}
-
    -
  • Departure wind: {{ briefingSnapshot.weather.depWind }}
  • -
  • Departure QNH: {{ briefingSnapshot.weather.depQnh }}
  • -
  • Arrival wind: {{ briefingSnapshot.weather.arrWind }}
  • -
  • Arrival QNH: {{ briefingSnapshot.weather.arrQnh }}
  • -
+
+
+
+
+ Route preview +
+ mdi-map-marker-path + Flight deck setup +
+
    +
  • Push: {{ briefingSnapshot.codes.push }}
  • +
  • ATIS: Information {{ briefingSnapshot.departure.atis }}
  • +
  • Delivery: {{ briefingSnapshot.departure.freq }} · {{ briefingSnapshot.departure.freqWords }}
  • +
+
+
+ Departure +
+ mdi-airplane-takeoff + Departure flow +
+
    +
  • Stand: {{ briefingSnapshot.departure.stand || 'As assigned' }}
  • +
  • Taxi: {{ briefingSnapshot.departure.taxiRoute || 'As assigned' }}
  • +
  • SID: {{ briefingSnapshot.departure.sid }} · {{ briefingSnapshot.departure.transition }}
  • +
  • Initial altitude: {{ briefingSnapshot.altitudes.initial }}
  • +
+
+
+ Arrival +
+ mdi-airplane-landing + Arrival setup +
+
    +
  • STAR: {{ briefingSnapshot.arrival.star }} · {{ briefingSnapshot.arrival.transition }}
  • +
  • Approach: {{ briefingSnapshot.arrival.approach }}
  • +
  • Taxi-in: {{ briefingSnapshot.arrival.taxiRoute || 'As assigned' }}
  • +
  • Arrival stand: {{ briefingSnapshot.arrival.stand || 'As assigned' }}
  • +
+
+
+ Weather +
+ mdi-weather-cloudy + Weather snapshot +
+
    +
  • Departure wind: {{ briefingSnapshot.weather.depWind }}
  • +
  • Departure QNH: {{ briefingSnapshot.weather.depQnh }}
  • +
  • Arrival wind: {{ briefingSnapshot.weather.arrWind }}
  • +
  • Arrival QNH: {{ briefingSnapshot.weather.arrQnh }}
  • +
+
+
+
+ +
+
@@ -616,6 +810,26 @@
+
+
+
Mission controls
+

Wrap up, open the briefing again or build a brand-new flight whenever you need.

+
+
+ + + +
+
Briefing
@@ -725,7 +939,7 @@ Auto-fill
@@ -1089,6 +1303,8 @@ const draftPlanScenario = ref(null) const manualBaseScenario = ref(null) const briefingReturnStage = ref<'setup' | 'lessons'>('setup') const pendingAutoStart = ref(false) +const SIMBRIEF_STORAGE_KEY = 'opensquawk:last-simbrief-id' + const manualForm = reactive({ airlineCode: '', airlineCall: '', @@ -1121,6 +1337,70 @@ const flightPlanError = ref(null) const simbriefForm = reactive({ userId: '', loading: false }) const simbriefPlanMeta = ref<{ callsign: string; route: string } | null>(null) const lessonTrack = ref(null) +type ManualSection = 'departure' | 'arrival' | 'procedures' +const manualSectionsOpen = reactive>({ + departure: false, + arrival: false, + procedures: false +}) + +const manualCallsignPreview = computed(() => { + const code = sanitizeUpper(manualForm.airlineCode) + const rawNumber = (manualForm.flightNumber || '').replace(/\s+/g, '') + if (!code && !rawNumber) return 'Awaiting callsign' + return `${code}${rawNumber}`.trim() || 'Awaiting callsign' +}) + +const manualSpokenPreview = computed(() => { + const call = manualForm.airlineCall?.trim() || sanitizeUpper(manualForm.airlineCode) + const digits = (manualForm.flightNumber || '').replace(/\s+/g, '') + if (!call && !digits) { + return 'We\'ll announce your ICAO code and flight number automatically if you leave this blank.' + } + const words = digits ? digitsToWords(digits) : '' + const speech = [call, words].filter(Boolean).join(' ').trim() + return speech || 'Radio call will mirror your ICAO code and number.' +}) + +const manualRoutePreview = computed(() => { + const dep = sanitizeUpper(manualForm.departureIcao) + const arr = sanitizeUpper(manualForm.destinationIcao) + if (!dep && !arr) return 'Add departure and destination' + return `${dep || '----'} → ${arr || '----'}` +}) + +const manualDepartureSummary = computed(() => { + const icao = sanitizeUpper(manualForm.departureIcao) + const runway = sanitizeUpper(manualForm.departureRunway) + const city = manualForm.departureCity?.trim() + if (!icao && !runway && !city) return 'Set departure details' + const parts = [icao || '----'] + if (runway) parts.push(`RWY ${runway}`) + if (city) parts.push(city) + return parts.join(' · ') +}) + +const manualArrivalSummary = computed(() => { + const icao = sanitizeUpper(manualForm.destinationIcao) + const runway = sanitizeUpper(manualForm.arrivalRunway) + const city = manualForm.destinationCity?.trim() + if (!icao && !runway && !city) return 'Set arrival details' + const parts = [icao || '----'] + if (runway) parts.push(`RWY ${runway}`) + if (city) parts.push(city) + return parts.join(' · ') +}) + +const manualCodesSummary = computed(() => { + const parts: string[] = [] + if (manualForm.initialAltitude) parts.push(`Initial ${manualForm.initialAltitude} ft`) + if (manualForm.climbAltitude) parts.push(`Climb ${manualForm.climbAltitude} ft`) + if (manualForm.squawk) parts.push(`Squawk ${sanitizeUpper(manualForm.squawk)}`) + if (manualForm.pushDelay) parts.push(`Push +${manualForm.pushDelay} min`) + return parts.join(' • ') || 'Using default training altitudes and codes.' +}) + +const manualRemarksSummary = computed(() => manualForm.remarks?.trim() || 'Add notes for controllers or leave blank.') const flightPlanModes: Array<{ id: FlightPlanMode; title: string; icon: string; desc: string }> = [ { @@ -1132,17 +1412,34 @@ const flightPlanModes: Array<{ id: FlightPlanMode; title: string; icon: string; { id: 'manual', title: 'Manual setup', - icon: 'mdi-airplane-edit', + icon: 'mdi-airplane-cog', desc: 'Enter your own callsign, airports and procedures.' }, { id: 'simbrief', title: 'SimBrief import', - icon: 'mdi-clipboard-text-clock', + icon: 'mdi-clipboard-text-outline', desc: 'Pull your latest SimBrief dispatch and brief it in-game.' } ] +watch( + () => simbriefForm.userId, + value => { + if (typeof window === 'undefined') return + const trimmed = value.trim() + if (trimmed) { + window.localStorage.setItem(SIMBRIEF_STORAGE_KEY, trimmed) + } else { + window.localStorage.removeItem(SIMBRIEF_STORAGE_KEY) + } + } +) + +function toggleManualSection(section: ManualSection) { + manualSectionsOpen[section] = !manualSectionsOpen[section] +} + function cloneScenarioData(source: Scenario | null): Scenario | null { if (!source) return null if (typeof structuredClone === 'function') { @@ -1282,19 +1579,26 @@ function applyPlanToScenario(base: Scenario, plan: MissionPlanInput): Scenario { if (!scenario) return base const airlineCode = sanitizeUpper(plan.airlineCode) || scenario.airlineCode - const airlineCall = plan.airlineCall?.trim() || scenario.airlineCall + const airlineCall = plan.airlineCall?.trim() || scenario.airlineCall || airlineCode const rawFlightNumber = plan.flightNumber?.toString().trim() || scenario.flightNumber const normalizedFlightNumber = rawFlightNumber.replace(/\s+/g, '') const digitsOnly = normalizedFlightNumber.replace(/[^0-9]/g, '') || normalizedFlightNumber const callsign = plan.callsign?.trim().toUpperCase() || `${airlineCode}${normalizedFlightNumber}`.toUpperCase() scenario.airlineCode = airlineCode - scenario.airlineCall = airlineCall + scenario.airlineCall = airlineCall || airlineCode scenario.flightNumber = digitsOnly scenario.flightNumberWords = digitsToWords(digitsOnly) scenario.callsign = callsign scenario.callsignNato = lettersToNato(airlineCode) - scenario.radioCall = plan.radioCall?.trim() || `${airlineCall} ${digitsToWords(digitsOnly)}` + const numberWords = digitsOnly ? digitsToWords(digitsOnly) : '' + const computedRadio = [scenario.airlineCall, numberWords].filter(Boolean).join(' ').trim() + const radioOverride = plan.radioCall?.trim() + if (radioOverride) { + scenario.radioCall = radioOverride + } else if (computedRadio) { + scenario.radioCall = computedRadio + } const dep = plan.departure || {} if (isNonEmpty(dep.icao)) scenario.airport.icao = sanitizeUpper(dep.icao) @@ -1450,7 +1754,6 @@ function enterBriefingFromSetup() { function buildManualScenario(): Scenario | null { const errors: string[] = [] if (!isNonEmpty(manualForm.airlineCode)) errors.push('Airline ICAO required') - if (!isNonEmpty(manualForm.airlineCall)) errors.push('Spoken call sign required') if (!isNonEmpty(manualForm.flightNumber)) errors.push('Flight number required') if (!isNonEmpty(manualForm.departureIcao)) errors.push('Departure ICAO required') if (!isNonEmpty(manualForm.destinationIcao)) errors.push('Destination ICAO required') @@ -1597,6 +1900,23 @@ function openMissionBriefing() { moduleStage.value = 'briefing' } +function restartCurrentMission() { + if (!current.value) return + const moduleId = current.value.id + if (moduleId && missionPlans[moduleId]) delete missionPlans[moduleId] + draftPlanScenario.value = null + manualBaseScenario.value = null + simbriefPlanMeta.value = null + scenario.value = null + flightPlanError.value = null + manualErrors.value = [] + flightPlanMode.value = 'manual' + moduleStage.value = 'setup' + briefingReturnStage.value = 'setup' + pendingAutoStart.value = false + initSetupState(null) +} + function startLessonsForCurrent() { if (!current.value) return const mod = current.value @@ -2373,8 +2693,8 @@ function modulePrimaryLabel(modId: string) { function modulePrimaryIcon(modId: string) { if (moduleCompleted(modId)) return 'mdi-history' - if (!moduleHasProgress(modId)) return 'mdi-rocket-launch' - return 'mdi-progress-clock' + if (!moduleHasProgress(modId)) return 'mdi-rocket' + return 'mdi-clock-outline' } function moduleSecondaryLabel(modId: string) { @@ -2395,8 +2715,8 @@ function moduleStatusText(modId: string) { function moduleStatusIcon(modId: string) { if (!isModuleUnlocked(modId)) return 'mdi-lock-alert' if (moduleCompleted(modId)) return 'mdi-medal-outline' - if (moduleHasProgress(modId)) return 'mdi-timer-sand-complete' - return 'mdi-rocket-launch-outline' + if (moduleHasProgress(modId)) return 'mdi-timer-sand' + return 'mdi-rocket-outline' } function tileStatusClass(modId: string) { @@ -2413,9 +2733,9 @@ function lessonScoreLabel(modId: string, lesId: string) { function lessonScoreIcon(modId: string, lesId: string) { const score = bestScore(modId, lesId) - if (!score) return 'mdi-star-four-points-outline' - if (score >= 80) return 'mdi-star-circle' - return 'mdi-progress-check' + if (!score) return 'mdi-star-outline' + if (score >= 80) return 'mdi-star-circle-outline' + return 'mdi-check-circle-outline' } function lessonScoreClass(modId: string, lesId: string) { @@ -2464,7 +2784,7 @@ const dailyObjectives = computed(() => { progress: readbacks, goal: 3, status: readbacks >= 3 ? 'Goal reached' : `${Math.max(0, 3 - Math.min(readbacks, 3))} more ≥80%`, - icon: 'mdi-target-account', + icon: 'mdi-target', complete: readbacks >= 3 }, { @@ -2977,6 +3297,12 @@ onMounted(() => { if (typeof window !== 'undefined' && 'speechSynthesis' in window) { window.speechSynthesis.getVoices() } + if (typeof window !== 'undefined') { + const storedId = window.localStorage.getItem(SIMBRIEF_STORAGE_KEY) + if (storedId) { + simbriefForm.userId = storedId + } + } }) @@ -4610,26 +4936,122 @@ onMounted(() => { justify-content: center; } -.manual-section { - display: flex; - flex-direction: column; - gap: 12px; +.manual-grid { + display: grid; + gap: 20px; + grid-template-columns: minmax(0, 1fr) minmax(260px, 320px); + align-items: start; } -.section-title { +.manual-form { display: flex; - align-items: center; - gap: 8px; + flex-direction: column; + gap: 16px; +} + +.manual-card { + border-radius: 18px; + border: 1px solid color-mix(in srgb, var(--text) 12%, transparent); + background: color-mix(in srgb, var(--text) 4%, transparent); + padding: 18px; + display: flex; + flex-direction: column; + gap: 14px; + box-shadow: 0 16px 34px rgba(2, 6, 23, .4); +} + +.manual-card--intro { + background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 10%, transparent), color-mix(in srgb, var(--bg2) 70%, transparent)); + border-color: color-mix(in srgb, var(--accent) 40%, transparent); +} + +.manual-card-header { + display: flex; + gap: 12px; + align-items: flex-start; +} + +.manual-card-icon { + width: 36px; + height: 36px; + border-radius: 12px; + display: grid; + place-items: center; + background: color-mix(in srgb, var(--accent) 22%, transparent); + color: var(--accent); +} + +.manual-card-title { font-weight: 600; letter-spacing: .08em; text-transform: uppercase; font-size: 13px; } +.required-grid { + display: grid; + gap: 12px; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); +} + +.required-dot { + margin-left: 4px; + color: var(--accent); + font-size: 14px; +} + +.manual-card--optional .manual-card-title { + display: flex; + align-items: center; + gap: 8px; +} + +.optional-chip { + font-size: 11px; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--t3); + border: 1px solid color-mix(in srgb, var(--text) 22%, transparent); + border-radius: 999px; + padding: 2px 8px; +} + +.optional-toggle { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + background: transparent; + border: 0; + color: inherit; + text-align: left; + padding: 0; + cursor: pointer; +} + +.optional-body { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.manual-card--optional .chevron { + transition: transform .2s ease; +} + +.manual-card--optional.is-open .chevron { + transform: rotate(-180deg); +} + +.optional-fields { + padding-top: 8px; +} + .manual-panel .field-grid { display: grid; gap: 12px; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); } .field-grid.compact { @@ -4663,6 +5085,129 @@ onMounted(() => { color: var(--t3); } +.manual-preview { + position: sticky; + top: 90px; +} + +.preview-card { + border-radius: 18px; + border: 1px solid color-mix(in srgb, var(--accent) 28%, transparent); + background: linear-gradient(160deg, color-mix(in srgb, var(--accent) 12%, transparent), color-mix(in srgb, var(--bg2) 70%, transparent)); + padding: 20px; + display: flex; + flex-direction: column; + gap: 18px; + box-shadow: 0 22px 40px rgba(2, 6, 23, .45); +} + +.preview-header { + display: flex; + gap: 12px; + align-items: flex-start; +} + +.preview-title { + font-weight: 600; + letter-spacing: .06em; + text-transform: uppercase; + font-size: 12px; +} + +.preview-section { + display: flex; + flex-direction: column; + gap: 6px; +} + +.preview-label { + font-size: 12px; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--t3); +} + +.preview-value { + font-size: 18px; + font-weight: 600; +} + +.preview-sub { + line-height: 1.3; +} + +.preview-pills { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.preview-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + border-radius: 999px; + background: color-mix(in srgb, var(--bg2) 80%, transparent); + font-size: 12px; + letter-spacing: .04em; + text-transform: uppercase; +} + +.manual-actions { + margin-top: 4px; + justify-content: flex-end; +} + +.collapse-enter-active, +.collapse-leave-active { + transition: all .2s ease; +} + +.collapse-enter-from, +.collapse-leave-to { + opacity: 0; + transform: translateY(-6px); +} + +.mission-controls { + margin-top: 16px; + padding: 14px 16px; + border-radius: 16px; + border: 1px solid color-mix(in srgb, var(--text) 12%, transparent); + background: color-mix(in srgb, var(--text) 6%, transparent); + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: space-between; +} + +.mission-controls-text { + display: flex; + flex-direction: column; + gap: 4px; + max-width: 320px; +} + +.mission-controls-title { + font-size: 12px; + letter-spacing: .1em; + text-transform: uppercase; + color: var(--t3); +} + +.mission-controls-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.mission-controls-actions .btn { + flex: 1 1 140px; + justify-content: center; +} + .form-errors { display: flex; flex-direction: column; @@ -4683,13 +5228,45 @@ onMounted(() => { .simbrief-panel { display: flex; flex-direction: column; - gap: 16px; + gap: 18px; } -.simbrief-callout { +.simbrief-header { display: flex; gap: 12px; - align-items: center; + align-items: flex-start; +} + +.simbrief-steps { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 12px; + font-size: 13px; + color: var(--t3); +} + +.simbrief-steps li { + display: flex; + gap: 12px; + align-items: flex-start; +} + +.step-number { + width: 26px; + height: 26px; + border-radius: 999px; + display: grid; + place-items: center; + font-weight: 600; + background: color-mix(in srgb, var(--accent) 18%, transparent); + color: var(--accent); +} + +.simbrief-note { + margin: 0; + color: var(--t3); } .simbrief-meta { @@ -4719,15 +5296,16 @@ onMounted(() => { font-size: 13px; } + .mission-briefing { display: flex; flex-direction: column; - gap: 20px; + gap: 24px; } -.briefing-header { +.briefing-top { display: flex; - gap: 16px; + gap: 20px; align-items: center; flex-wrap: wrap; } @@ -4778,6 +5356,95 @@ onMounted(() => { text-transform: uppercase; } +.briefing-summary-grid { + display: grid; + gap: 14px; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); +} + +.summary-card { + border-radius: 16px; + border: 1px solid color-mix(in srgb, var(--text) 12%, transparent); + background: color-mix(in srgb, var(--text) 5%, transparent); + padding: 14px; + display: flex; + gap: 12px; + align-items: flex-start; +} + +.summary-label { + font-size: 11px; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--t3); +} + +.summary-value { + font-weight: 600; + font-size: 16px; +} + +.summary-sub { + font-size: 13px; +} + +.briefing-columns { + display: flex; + gap: 20px; + align-items: stretch; + flex-wrap: wrap; +} + +.briefing-main { + flex: 1 1 0; + min-width: 260px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.briefing-sidebar { + flex: 0 0 260px; + display: flex; +} + +.briefing-checklist-card { + height: 100%; +} + +.briefing-checklist { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 14px; +} + +.briefing-checklist li { + display: flex; + gap: 12px; + align-items: flex-start; +} + +.check-number { + width: 26px; + height: 26px; + border-radius: 999px; + display: grid; + place-items: center; + font-weight: 600; + background: color-mix(in srgb, var(--accent) 18%, transparent); + color: var(--accent); +} + +.check-title { + font-weight: 600; + letter-spacing: .04em; + text-transform: uppercase; + font-size: 12px; +} + .briefing-grid { display: grid; gap: 16px; @@ -4852,10 +5519,32 @@ onMounted(() => { .briefing-actions .btn { flex: 1 1 100%; } - .briefing-header { + .manual-grid { + grid-template-columns: 1fr; + } + .manual-preview { + position: static; + } + .briefing-top { flex-direction: column; align-items: flex-start; } + .briefing-columns { + flex-direction: column; + } + .briefing-sidebar { + flex: 1 1 auto; + } + .mission-controls { + flex-direction: column; + align-items: flex-start; + } + .mission-controls-actions { + width: 100%; + } + .mission-controls-actions .btn { + flex: 1 1 100%; + } }