feat(design): Projector Noir theme - tungsten amber on film-black

Full visual refonte replacing the generic neon-blue glass look:

- palette: warm film-black surface ladder, tungsten amber primary,
  warm taupe secondary, verdigris teal tertiary; semantic and
  category colors retuned to match (coral live, gold movies)
- typography: Syne (display/headlines) + Instrument Sans (body/UI)
  replace Space Grotesk/Inter/Outfit across all screens
- glassmorphism tokens warmed (amber inner glow, warm-white borders)
- web player theme.css + index.html loader mirror the new tokens
- hardcoded Colors.blue* in streaming settings and recordings tabs
  now use AppColors tokens
- DESIGN_SYSTEM.md updated to v3.0 Projector Noir

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Fable 5 committed 2026-06-10 14:52:45 +02:00
1 parent 9595afcc99
commit e332895d7d
33 files changed
+392 -397

No files matched your search

+80 -79
View File
@@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
/// Cyber-Cinematic Glass Design System — Material 3 Token Palette
/// Projector Noir Design System — Material 3 Token Palette
///
/// Extracted from the Stitch design system for XtremFlow.
/// Warm tungsten light on deep film-black, like a projector beam cutting
/// through a dark screening room. Verdigris teal as the counterpoint accent.
/// Optimized for OLED displays and low-light environments.
/// Primary accent: Neon blue (#4b8eff / #adc6ff)
/// Primary accent: Tungsten amber (#F39A1F / #FFB35C)
class AppColors {
AppColors._();
@@ -13,58 +14,58 @@ class AppColors {
static Color get border => outlineVariant;
// ============ SURFACE / ON SURFACE ============
static const Color onSurface = Color(0xFFe3e2e7);
static const Color onSurfaceVariant = Color(0xFFc1c6d7);
static const Color onSurface = Color(0xFFECE6DA);
static const Color onSurfaceVariant = Color(0xFFCDC4B4);
// ============ BASE LEVEL 0 ============
static const Color baseLevel0 = Color(0xFF0F1014);
static const Color baseLevel0 = Color(0xFF0B0A08);
// ============ BACKGROUNDS ============
static const Color background = Color(0xFF121317);
static const Color onBackground = Color(0xFFe3e2e7);
static const Color background = Color(0xFF14110C);
static const Color onBackground = Color(0xFFECE6DA);
static const Color surface = Color(0xFF121317);
static const Color surfaceDim = Color(0xFF121317);
static const Color surfaceBright = Color(0xFF38393d);
static const Color surface = Color(0xFF14110C);
static const Color surfaceDim = Color(0xFF14110C);
static const Color surfaceBright = Color(0xFF3E382C);
// Surface Containers (Material 3 hierarchy)
static const Color surfaceContainerLowest = Color(0xFF0d0e12);
static const Color surfaceContainerLow = Color(0xFF1a1b20);
static const Color surfaceContainer = Color(0xFF1f1f24);
static const Color surfaceContainerHigh = Color(0xFF292a2e);
static const Color surfaceContainerHighest = Color(0xFF343439);
static const Color surfaceVariant = Color(0xFF343439);
static const Color surfaceContainerLowest = Color(0xFF0E0C09);
static const Color surfaceContainerLow = Color(0xFF1B1812);
static const Color surfaceContainer = Color(0xFF211D16);
static const Color surfaceContainerHigh = Color(0xFF2B261E);
static const Color surfaceContainerHighest = Color(0xFF363027);
static const Color surfaceVariant = Color(0xFF363027);
// ============ PRIMARY (Neon Blue) ============
static const Color primary = Color(0xFFadc6ff);
static const Color onPrimary = Color(0xFF002e69);
static const Color primaryContainer = Color(0xFF4b8eff);
static const Color onPrimaryContainer = Color(0xFF00285c);
static const Color inversePrimary = Color(0xFF005bc1);
static const Color primaryFixed = Color(0xFFd8e2ff);
static const Color primaryFixedDim = Color(0xFFadc6ff);
static const Color onPrimaryFixed = Color(0xFF001a41);
static const Color onPrimaryFixedVariant = Color(0xFF004493);
// ============ PRIMARY (Tungsten Amber) ============
static const Color primary = Color(0xFFFFB35C);
static const Color onPrimary = Color(0xFF452800);
static const Color primaryContainer = Color(0xFFF39A1F);
static const Color onPrimaryContainer = Color(0xFF2E1B00);
static const Color inversePrimary = Color(0xFF8F5B00);
static const Color primaryFixed = Color(0xFFFFDDB3);
static const Color primaryFixedDim = Color(0xFFFFB35C);
static const Color onPrimaryFixed = Color(0xFF2A1700);
static const Color onPrimaryFixedVariant = Color(0xFF6B4300);
// ============ SECONDARY ============
static const Color secondary = Color(0xFFc6c5cf);
static const Color onSecondary = Color(0xFF2f3037);
static const Color secondaryContainer = Color(0xFF4a4b53);
static const Color onSecondaryContainer = Color(0xFFbcbbc4);
static const Color secondaryFixed = Color(0xFFe3e1eb);
static const Color secondaryFixedDim = Color(0xFFc6c5cf);
static const Color onSecondaryFixed = Color(0xFF1a1b22);
static const Color onSecondaryFixedVariant = Color(0xFF46464e);
// ============ SECONDARY (Warm Taupe) ============
static const Color secondary = Color(0xFFD3C6B2);
static const Color onSecondary = Color(0xFF382F21);
static const Color secondaryContainer = Color(0xFF504535);
static const Color onSecondaryContainer = Color(0xFFD8CCB9);
static const Color secondaryFixed = Color(0xFFF0E4D0);
static const Color secondaryFixedDim = Color(0xFFD3C6B2);
static const Color onSecondaryFixed = Color(0xFF231B0E);
static const Color onSecondaryFixedVariant = Color(0xFF4D4232);
// ============ TERTIARY ============
static const Color tertiary = Color(0xFFc6c6c7);
static const Color onTertiary = Color(0xFF2f3131);
static const Color tertiaryContainer = Color(0xFF909191);
static const Color onTertiaryContainer = Color(0xFF282a2a);
static const Color tertiaryFixed = Color(0xFFe2e2e2);
static const Color tertiaryFixedDim = Color(0xFFc6c6c7);
static const Color onTertiaryFixed = Color(0xFF1a1c1c);
static const Color onTertiaryFixedVariant = Color(0xFF454747);
// ============ TERTIARY (Verdigris Teal) ============
static const Color tertiary = Color(0xFFA4CCBE);
static const Color onTertiary = Color(0xFF0F352B);
static const Color tertiaryContainer = Color(0xFF2F5A4C);
static const Color onTertiaryContainer = Color(0xFFC0E8D9);
static const Color tertiaryFixed = Color(0xFFC0E8D9);
static const Color tertiaryFixedDim = Color(0xFFA4CCBE);
static const Color onTertiaryFixed = Color(0xFF04201A);
static const Color onTertiaryFixedVariant = Color(0xFF274B40);
// ============ ERROR ============
static const Color error = Color(0xFFffb4ab);
@@ -73,61 +74,61 @@ class AppColors {
static const Color onErrorContainer = Color(0xFFffdad6);
// ============ OUTLINE ============
static const Color outline = Color(0xFF8b90a0);
static const Color outlineVariant = Color(0xFF414755);
static const Color outline = Color(0xFF998F7C);
static const Color outlineVariant = Color(0xFF4A4334);
// ============ INVERSE SURFACE ============
static const Color inverseSurface = Color(0xFFe3e2e7);
static const Color inverseOnSurface = Color(0xFF2f3035);
static const Color inverseSurface = Color(0xFFECE6DA);
static const Color inverseOnSurface = Color(0xFF332F26);
// ============ SURFACE TINT ============
static const Color surfaceTint = Color(0xFFadc6ff);
static const Color surfaceTint = Color(0xFFFFB35C);
// ============ TEXT / CONTENT COLORS ============
static const Color textPrimary = Color(0xFFe3e2e7);
static const Color textSecondary = Color(0xFFc1c6d7);
static const Color textTertiary = Color(0xFF8b90a0);
static const Color textPrimary = Color(0xFFECE6DA);
static const Color textSecondary = Color(0xFFCDC4B4);
static const Color textTertiary = Color(0xFF998F7C);
// ============ STATUS COLORS ============
static const Color success = Color(0xFF00E676);
static const Color success = Color(0xFF6BD89B);
static const Color warning = Color(0xFFFFAB00);
static const Color info = Color(0xFF2979FF);
static const Color info = Color(0xFF64B6AC);
// ============ CATEGORY COLORS (kept for semantic meaning) ============
static const Color live = Color(0xFFFF5252);
static const Color movies = Color(0xFF448AFF);
static const Color series = Color(0xFF69F0AE);
static const Color live = Color(0xFFFF6E5E);
static const Color movies = Color(0xFFE8B64C);
static const Color series = Color(0xFF5FC9AE);
// ============ RATING / PREMIUM ============
static const Color ratingGold = Color(0xFFFFD700);
static const Color ratingGold = Color(0xFFFFD166);
static const Color premiumGold = Color(0xFFFFA000);
// ============ GLASSMORPHISM LEVELS ============
static const Color glassLevel1Bg = Color(0x66121317); // 40% opacity
static const Color glassLevel1Border = Color(0x1AFFFFFF); // 10% white
static const Color glassLevel1Bg = Color(0x6614110C); // 40% opacity
static const Color glassLevel1Border = Color(0x1AFFE9C9); // 10% warm white
static const Color glassLevel2Bg = Color(0x99121317); // 60% opacity
static const Color glassLevel2Border = Color(0x1AFFFFFF); // 10% white
static const Color glassLevel2InnerGlow = Color(0x33adc6ff); // 20% primary top-left
static const Color glassLevel2Bg = Color(0x9914110C); // 60% opacity
static const Color glassLevel2Border = Color(0x1AFFE9C9); // 10% warm white
static const Color glassLevel2InnerGlow = Color(0x33FFB35C); // 20% primary top-left
// ============ GRADIENTS ============
static const LinearGradient primaryGradient = LinearGradient(
colors: [Color(0xFF007AFF), Color(0xFF00C6FF)],
colors: [Color(0xFFE8941A), Color(0xFFFFC36A)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
);
static const LinearGradient backgroundGradient = LinearGradient(
colors: [Color(0xFF0F1014), Color(0xFF121317)],
colors: [Color(0xFF0B0A08), Color(0xFF14110C)],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
);
// ============ GLOW / SHADOWS ============
static Color glowPrimary(double opacity) =>
const Color(0xFF007AFF).withOpacity(opacity);
const Color(0xFFF39A1F).withOpacity(opacity);
static Color glowPrimaryDim(double opacity) =>
const Color(0xFFadc6ff).withOpacity(opacity);
const Color(0xFFFFB35C).withOpacity(opacity);
// ============ COLOR SCHEMES ============
static ColorScheme get darkColorScheme => const ColorScheme.dark(
@@ -162,24 +163,24 @@ class AppColors {
brightness: Brightness.dark,
);
// Light scheme is minimal — Stitch is dark-only
// Light scheme is minimal — Projector Noir is dark-first
static ColorScheme get lightColorScheme => const ColorScheme.light(
primary: Color(0xFF005bc1),
primary: Color(0xFF8F5B00),
onPrimary: Colors.white,
primaryContainer: Color(0xFFd8e2ff),
onPrimaryContainer: Color(0xFF001a41),
secondary: Color(0xFF5e5c68),
primaryContainer: Color(0xFFFFDDB3),
onPrimaryContainer: Color(0xFF2A1700),
secondary: Color(0xFF6F6353),
onSecondary: Colors.white,
secondaryContainer: Color(0xFFe3e1eb),
onSecondaryContainer: Color(0xFF1a1b22),
secondaryContainer: Color(0xFFF0E4D0),
onSecondaryContainer: Color(0xFF231B0E),
error: Color(0xFFba1a1a),
onError: Colors.white,
errorContainer: Color(0xFFFFdad6),
onErrorContainer: Color(0xFF410002),
surface: Color(0xFFF9F9FF),
onSurface: Color(0xFF1a1b22),
outline: Color(0xFF757680),
outlineVariant: Color(0xFFc5c6d0),
surface: Color(0xFFFFF8F0),
onSurface: Color(0xFF1E1B15),
outline: Color(0xFF7F766A),
outlineVariant: Color(0xFFD0C5B4),
brightness: Brightness.light,
);
}
+42 -42
View File
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'app_colors.dart';
/// XtremFlow Cyber-Cinematic Glass Theme
/// XtremFlow Projector Noir Theme
///
/// Design system extracted from Google Stitch.
/// Fonts: Space Grotesk (headlines) + Inter (body/labels)
/// Style: Dark-only glassmorphism with neon blue accents.
/// Warm tungsten light on deep film-black.
/// Fonts: Syne (headlines, art-house geometric) + Instrument Sans (body/labels)
/// Style: Dark-first glassmorphism with tungsten amber accents.
class AppTheme {
AppTheme._();
@@ -39,14 +39,14 @@ class AppTheme {
static const Curve curveSmooth = Curves.fastOutSlowIn;
// ============ TYPOGRAPHY HELPERS ============
static TextStyle _spaceGrotesk({
static TextStyle _syne({
required double fontSize,
required FontWeight fontWeight,
double? letterSpacing,
double? height,
Color color = AppColors.onSurface,
}) {
return GoogleFonts.spaceGrotesk(
return GoogleFonts.syne(
fontSize: fontSize,
fontWeight: fontWeight,
letterSpacing: letterSpacing,
@@ -55,14 +55,14 @@ class AppTheme {
);
}
static TextStyle _inter({
static TextStyle _instrumentSans({
required double fontSize,
required FontWeight fontWeight,
double? letterSpacing,
double? height,
Color color = AppColors.onSurface,
}) {
return GoogleFonts.inter(
return GoogleFonts.instrumentSans(
fontSize: fontSize,
fontWeight: fontWeight,
letterSpacing: letterSpacing,
@@ -81,85 +81,85 @@ class AppTheme {
// Typography
textTheme: TextTheme(
// Display / Headline (Space Grotesk)
displayLarge: _spaceGrotesk(
// Display / Headline (Syne)
displayLarge: _syne(
fontSize: 48,
fontWeight: FontWeight.w700,
letterSpacing: -0.02,
height: 1.1,
),
displayMedium: _spaceGrotesk(
displayMedium: _syne(
fontSize: 32,
fontWeight: FontWeight.w600,
letterSpacing: -0.01,
height: 1.2,
),
displaySmall: _spaceGrotesk(
displaySmall: _syne(
fontSize: 24,
fontWeight: FontWeight.w500,
height: 1.3,
),
headlineLarge: _spaceGrotesk(
headlineLarge: _syne(
fontSize: 32,
fontWeight: FontWeight.w600,
letterSpacing: -0.01,
height: 1.2,
),
headlineMedium: _spaceGrotesk(
headlineMedium: _syne(
fontSize: 24,
fontWeight: FontWeight.w500,
height: 1.3,
),
headlineSmall: _spaceGrotesk(
headlineSmall: _syne(
fontSize: 20,
fontWeight: FontWeight.w500,
height: 1.3,
),
titleLarge: _spaceGrotesk(
titleLarge: _syne(
fontSize: 20,
fontWeight: FontWeight.w600,
),
titleMedium: _inter(
titleMedium: _instrumentSans(
fontSize: 16,
fontWeight: FontWeight.w600,
),
titleSmall: _inter(
titleSmall: _instrumentSans(
fontSize: 14,
fontWeight: FontWeight.w600,
),
// Body (Inter)
bodyLarge: _inter(
// Body (Instrument Sans)
bodyLarge: _instrumentSans(
fontSize: 18,
fontWeight: FontWeight.w400,
height: 1.6,
color: AppColors.onSurfaceVariant,
),
bodyMedium: _inter(
bodyMedium: _instrumentSans(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.5,
color: AppColors.onSurfaceVariant,
),
bodySmall: _inter(
bodySmall: _instrumentSans(
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
color: AppColors.onSurfaceVariant,
),
// Labels (Inter with tracking)
labelLarge: _inter(
// Labels (Instrument Sans with tracking)
labelLarge: _instrumentSans(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.05,
height: 1.2,
),
labelMedium: _inter(
labelMedium: _instrumentSans(
fontSize: 12,
fontWeight: FontWeight.w500,
letterSpacing: 0.02,
height: 1.2,
),
labelSmall: _inter(
labelSmall: _instrumentSans(
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.02,
@@ -174,13 +174,13 @@ class AppTheme {
elevation: 0,
centerTitle: true,
scrolledUnderElevation: 0,
titleTextStyle: _spaceGrotesk(
titleTextStyle: _syne(
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
// Inputs: Dark filled with blue glow on focus
// Inputs: Dark filled with amber glow on focus
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.surfaceContainerLow,
@@ -204,12 +204,12 @@ class AppTheme {
borderRadius: BorderRadius.circular(radiusMd),
borderSide: const BorderSide(color: AppColors.error, width: 1),
),
hintStyle: _inter(
hintStyle: _instrumentSans(
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.outline,
),
labelStyle: _inter(
labelStyle: _instrumentSans(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.onSurfaceVariant,
@@ -226,7 +226,7 @@ class AppTheme {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
),
textStyle: _inter(
textStyle: _instrumentSans(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.05,
@@ -253,7 +253,7 @@ class AppTheme {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
),
textStyle: _inter(
textStyle: _instrumentSans(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.05,
@@ -266,7 +266,7 @@ class AppTheme {
style: TextButton.styleFrom(
foregroundColor: AppColors.primary,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
textStyle: _inter(
textStyle: _instrumentSans(
fontSize: 14,
fontWeight: FontWeight.w600,
),
@@ -292,11 +292,11 @@ class AppTheme {
borderRadius: BorderRadius.circular(radiusXl),
side: const BorderSide(color: AppColors.glassLevel2Border, width: 1),
),
titleTextStyle: _spaceGrotesk(
titleTextStyle: _syne(
fontSize: 20,
fontWeight: FontWeight.w600,
),
contentTextStyle: _inter(
contentTextStyle: _instrumentSans(
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.onSurfaceVariant,
@@ -314,7 +314,7 @@ class AppTheme {
// Snackbars
snackBarTheme: SnackBarThemeData(
backgroundColor: AppColors.surfaceContainerHigh,
contentTextStyle: _inter(
contentTextStyle: _instrumentSans(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.onSurface,
@@ -336,12 +336,12 @@ class AppTheme {
shape: StadiumBorder(
side: BorderSide(color: AppColors.primary.withOpacity(0.3)),
),
labelStyle: _inter(
labelStyle: _instrumentSans(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.onSurface,
),
secondaryLabelStyle: _inter(
secondaryLabelStyle: _instrumentSans(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.onPrimaryContainer,
@@ -353,8 +353,8 @@ class AppTheme {
labelColor: AppColors.onSurface,
unselectedLabelColor: AppColors.outline,
indicatorColor: AppColors.primaryContainer,
labelStyle: _inter(fontSize: 14, fontWeight: FontWeight.w600),
unselectedLabelStyle: _inter(fontSize: 14, fontWeight: FontWeight.w500),
labelStyle: _instrumentSans(fontSize: 14, fontWeight: FontWeight.w600),
unselectedLabelStyle: _instrumentSans(fontSize: 14, fontWeight: FontWeight.w500),
),
// Divider
@@ -389,10 +389,10 @@ class AppTheme {
);
}
// Light theme is minimal — Stitch is dark-only
// Light theme is minimal — Projector Noir is dark-first
static ThemeData get lightTheme => ThemeData.light(useMaterial3: true).copyWith(
colorScheme: AppColors.lightColorScheme,
scaffoldBackgroundColor: AppColors.lightColorScheme.surface,
textTheme: GoogleFonts.interTextTheme(ThemeData.light().textTheme),
textTheme: GoogleFonts.instrumentSansTextTheme(ThemeData.light().textTheme),
);
}