import 'package:flutter/material.dart'; /// PilotVault design system (mobile) — light-default brand theme. /// Signal Blue accent over Vault Navy and cool slate neutrals; Space Grotesk /// for structure, Space Mono for data/telemetry and uppercase eyebrow labels. class PV { PV._(); static const String fontSans = 'Space Grotesk'; static const String fontMono = 'Space Mono'; // Brand static const Color navy = Color(0xFF0F1E3D); // Vault Navy static const Color accent = Color(0xFF3D7BF0); // Signal Blue static const Color accentHover = Color(0xFF2B62CC); // Status (names kept: ready=green, caution=amber, warning=red fault) static const Color ready = Color(0xFF1F8A5B); static const Color readyFg = Color(0xFF177049); static const Color caution = Color(0xFFD9852B); static const Color cautionFg = Color(0xFFB86C1B); static const Color warning = Color(0xFFD64545); static const Color warningFg = Color(0xFFB83232); // Surfaces (light) static const Color surface0 = Color(0xFFEEF0F3); // Cloud app ground static const Color surface1 = Color(0xFFFFFFFF); // card / chrome static const Color surface2 = Color(0xFFF6F7F9); // inset tiles / inputs // Text static const Color ink = navy; // primary static const Color inkSecondary = Color(0xFF5A6B85); // steel static const Color inkMuted = Color(0xFF97A1B0); // slate-400 // Hairlines static const Color line = Color(0xFFDCE0E7); static const Color lineStrong = Color(0xFFC5CCD7); static const double radius = 14; // cards static const double radiusCtl = 10; // controls static const List shadowXs = [ BoxShadow(color: Color(0x0F0F1E3D), blurRadius: 2, offset: Offset(0, 1)), ]; // Type scale — mono for data, sans for chrome static const TextStyle telemetry = TextStyle( fontFamily: fontMono, fontSize: 30, fontWeight: FontWeight.w500, height: 1.0, color: ink, fontFeatures: [FontFeature.tabularFigures()], ); static const TextStyle mode = TextStyle( fontFamily: fontSans, fontSize: 18, fontWeight: FontWeight.w600, letterSpacing: -0.4, color: ink, ); static const TextStyle body = TextStyle(fontFamily: fontSans, fontSize: 14, color: ink); static const TextStyle caption = TextStyle(fontFamily: fontSans, fontSize: 12, color: inkSecondary); // Mono ALL-CAPS eyebrow (telemetry field names / section labels) static const TextStyle label = TextStyle( fontFamily: fontMono, fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 1.5, color: inkMuted, ); // Mono value (serials, coordinates, counts) static const TextStyle mono = TextStyle( fontFamily: fontMono, fontSize: 14, fontWeight: FontWeight.w700, color: ink, fontFeatures: [FontFeature.tabularFigures()], ); static ThemeData theme() { final ThemeData base = ThemeData.light(useMaterial3: true); final ColorScheme scheme = ColorScheme.fromSeed( seedColor: accent, brightness: Brightness.light, ).copyWith(primary: accent, surface: surface1, onSurface: ink); OutlineInputBorder borderOf(Color c, [double w = 1]) => OutlineInputBorder( borderRadius: BorderRadius.circular(radiusCtl), borderSide: BorderSide(color: c, width: w), ); return base.copyWith( colorScheme: scheme, scaffoldBackgroundColor: surface0, dividerColor: line, textTheme: base.textTheme.apply( fontFamily: fontSans, bodyColor: ink, displayColor: ink, ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: surface2, isDense: true, hintStyle: const TextStyle(color: inkMuted), labelStyle: const TextStyle(color: inkSecondary), prefixIconColor: inkMuted, enabledBorder: borderOf(line), focusedBorder: borderOf(accent, 2), border: borderOf(line), ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( backgroundColor: accent, foregroundColor: Colors.white, disabledBackgroundColor: surface2, disabledForegroundColor: inkMuted, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusCtl)), padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18), textStyle: const TextStyle(fontFamily: fontSans, fontWeight: FontWeight.w600, fontSize: 14), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( foregroundColor: ink, backgroundColor: surface1, side: const BorderSide(color: lineStrong), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusCtl)), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), textStyle: const TextStyle(fontFamily: fontSans, fontWeight: FontWeight.w600, fontSize: 14), ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom(foregroundColor: inkSecondary), ), snackBarTheme: SnackBarThemeData( backgroundColor: navy, contentTextStyle: const TextStyle(fontFamily: fontSans, color: Colors.white), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(radiusCtl)), behavior: SnackBarBehavior.floating, ), ); } } /// PilotVault Vector mark — two offset chevrons. Back wing Signal Blue, /// front wing the foreground color (navy on light). class PvBrandMark extends StatelessWidget { const PvBrandMark({super.key, this.size = 28, this.frontColor}); final double size; final Color? frontColor; @override Widget build(BuildContext context) { return CustomPaint( size: Size(size, size), painter: _ChevronPainter(frontColor ?? PV.ink), ); } } class _ChevronPainter extends CustomPainter { _ChevronPainter(this.front); final Color front; @override void paint(Canvas canvas, Size size) { final double s = size.width / 48.0; Paint stroke(Color c) => Paint() ..color = c ..style = PaintingStyle.stroke ..strokeWidth = 4 * s ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round; final Path back = Path() ..moveTo(8 * s, 30 * s) ..lineTo(19 * s, 17 * s) ..lineTo(30 * s, 30 * s); final Path frontWing = Path() ..moveTo(18 * s, 33 * s) ..lineTo(29 * s, 20 * s) ..lineTo(40 * s, 33 * s); canvas.drawPath(back, stroke(PV.accent)); canvas.drawPath(frontWing, stroke(front)); } @override bool shouldRepaint(covariant _ChevronPainter old) => old.front != front; } /// A card surface: white face, hairline border, soft cool shadow. class PvPanel extends StatelessWidget { const PvPanel({super.key, required this.child, this.padding = const EdgeInsets.all(16)}); final Widget child; final EdgeInsetsGeometry padding; @override Widget build(BuildContext context) { return Container( padding: padding, decoration: BoxDecoration( color: PV.surface1, borderRadius: BorderRadius.circular(PV.radius), border: Border.all(color: PV.line), boxShadow: PV.shadowXs, ), child: child, ); } } /// Mono uppercase eyebrow label (telemetry field / section label). class SectionLabel extends StatelessWidget { const SectionLabel(this.text, {super.key}); final String text; @override Widget build(BuildContext context) => Text(text.toUpperCase(), style: PV.label); } /// A status dot. Solid by default; brand carries status by color, not glow. class StatusDot extends StatelessWidget { const StatusDot(this.color, {super.key, this.glow = false, this.size = 9}); final Color color; final bool glow; final double size; @override Widget build(BuildContext context) { return Container( width: size, height: size, decoration: BoxDecoration( color: color, shape: BoxShape.circle, boxShadow: glow ? [BoxShadow(color: color.withValues(alpha: 0.35), blurRadius: 6)] : null, ), ); } } /// ───────────────────────────────────────────────────────────────────────────── /// Full semantic token set, theme-flipping (mirrors tokens/colors.css). The Go /// Fly and Album screens follow the device brightness; Flight Control overlays a /// live camera feed and is always dark, so it uses fixed glass colors (below). /// ───────────────────────────────────────────────────────────────────────────── class PVScheme { const PVScheme({ required this.brightness, required this.bgApp, required this.surface, required this.surface2, required this.surfaceInset, required this.surfaceRaised, required this.border, required this.borderStrong, required this.borderSubtle, required this.textPrimary, required this.textSecondary, required this.textTertiary, required this.textInverse, required this.accent, required this.accentHover, required this.accentSoft, required this.accentSoftFg, required this.success, required this.successSoft, required this.successFg, required this.warning, required this.warningSoft, required this.warningFg, required this.danger, required this.dangerSoft, required this.dangerFg, required this.shadowXs, required this.shadowSm, }); final Brightness brightness; final Color bgApp, surface, surface2, surfaceInset, surfaceRaised; final Color border, borderStrong, borderSubtle; final Color textPrimary, textSecondary, textTertiary, textInverse; final Color accent, accentHover, accentSoft, accentSoftFg; final Color success, successSoft, successFg; final Color warning, warningSoft, warningFg; final Color danger, dangerSoft, dangerFg; final List shadowXs, shadowSm; bool get isDark => brightness == Brightness.dark; static const PVScheme light = PVScheme( brightness: Brightness.light, bgApp: Color(0xFFEEF0F3), surface: Color(0xFFFFFFFF), surface2: Color(0xFFF6F7F9), surfaceInset: Color(0xFFEEF0F3), surfaceRaised: Color(0xFFFFFFFF), border: Color(0xFFDCE0E7), borderStrong: Color(0xFFC5CCD7), borderSubtle: Color(0xFFE6E9EE), textPrimary: Color(0xFF0F1E3D), textSecondary: Color(0xFF5A6B85), textTertiary: Color(0xFF97A1B0), textInverse: Color(0xFFFFFFFF), accent: Color(0xFF3D7BF0), accentHover: Color(0xFF2B62CC), accentSoft: Color(0xFFEAF1FE), accentSoftFg: Color(0xFF1F4CA0), success: Color(0xFF1F8A5B), successSoft: Color(0xFFDCF1E7), successFg: Color(0xFF177049), warning: Color(0xFFD9852B), warningSoft: Color(0xFFFBEBD5), warningFg: Color(0xFFB86C1B), danger: Color(0xFFD64545), dangerSoft: Color(0xFFFBE0E0), dangerFg: Color(0xFFB83232), shadowXs: [BoxShadow(color: Color(0x0F0F1E3D), blurRadius: 2, offset: Offset(0, 1))], shadowSm: [ BoxShadow(color: Color(0x0F0F1E3D), blurRadius: 3, offset: Offset(0, 1)), BoxShadow(color: Color(0x0A0F1E3D), blurRadius: 2, offset: Offset(0, 1)), ], ); static const PVScheme dark = PVScheme( brightness: Brightness.dark, bgApp: Color(0xFF0B1730), surface: Color(0xFF10203F), surface2: Color(0xFF142748), surfaceInset: Color(0xFF0B1730), surfaceRaised: Color(0xFF16294B), border: Color(0x1AFFFFFF), borderStrong: Color(0x2EFFFFFF), borderSubtle: Color(0x0FFFFFFF), textPrimary: Color(0xFFF4F7FC), textSecondary: Color(0xFF8FA0BE), textTertiary: Color(0xFF5E6E8C), textInverse: Color(0xFF0F1E3D), accent: Color(0xFF5B93F5), accentHover: Color(0xFF8FB4F6), accentSoft: Color(0x2E3D7BF0), accentSoftFg: Color(0xFF8FB4F6), success: Color(0xFF1F8A5B), successSoft: Color(0x381F8A5B), successFg: Color(0xFF5FD3A0), warning: Color(0xFFD9852B), warningSoft: Color(0x38D9852B), warningFg: Color(0xFFF0B26A), danger: Color(0xFFD64545), dangerSoft: Color(0x38D64545), dangerFg: Color(0xFFF08A8A), shadowXs: [BoxShadow(color: Color(0x59000000), blurRadius: 2, offset: Offset(0, 1))], shadowSm: [BoxShadow(color: Color(0x66000000), blurRadius: 3, offset: Offset(0, 1))], ); /// Resolves the scheme from the device brightness. static PVScheme of(BuildContext context) => MediaQuery.platformBrightnessOf(context) == Brightness.dark ? dark : light; } /// Fixed "glass" palette for the Flight Control overlay (always over a dark feed). class Glass { Glass._(); static const Color ink = Color(0xFFEAF0FA); static const Color pill = Color(0x80081020); // rgba(8,16,32,0.5) static const Color pillStrong = Color(0x8C081020); // rgba(8,16,32,0.55) static const Color accent = Color(0xEB3D7BF0); // rgba(61,123,240,0.92) static const Color hairline = Color(0x1FFFFFFF); // rgba(255,255,255,0.12) static const Color sat = Color(0xFF7FE0B0); static const Color rec = Color(0xFFD64545); static const Color subject = Color(0xFFF4C542); }