@use 'sass:map'; @use '@angular/material' as mat; @use 'styles'; @include mat.elevation-classes(); @include mat.app-background(); // ─── Typography ───────────────────────────────────────────────────────────── // The stack is declared once on html; every Material typography token and the // typography hierarchy read it through the variable. Inlined, the 103-char // stack was copied into ~90 declarations (about 7 KB of the initial CSS). $app-font-stack-value: "'DM Sans', 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif"; $app-font-stack: var(--app-font-family); html { --app-font-family: #{$app-font-stack-value}; } $my-typography: mat.m2-define-typography-config( $font-family: $app-font-stack, ); @include mat.typography-hierarchy($my-typography); // ─── M3 Themes ─────────────────────────────────────────────────────────────── // One primary color across the app. Azure is the closest M3 prebuilt to the // product's "active/selection" blue (#2f7bff / #78adff), so checkboxes, radio // buttons, raised CTAs, etc. now read as the same blue as rail selection. // // Cyan stays on tertiary so the EPG "now" indicator (which uses Material // tertiary surfaces) keeps its existing visual role. $light-theme: mat.define-theme( ( color: ( theme-type: light, primary: mat.$azure-palette, tertiary: mat.$cyan-palette, ), typography: ( plain-family: $app-font-stack, brand-family: $app-font-stack, ), ) ); $dark-theme: mat.define-theme( ( color: ( theme-type: dark, primary: mat.$azure-palette, tertiary: mat.$cyan-palette, ), typography: ( plain-family: $app-font-stack, brand-family: $app-font-stack, ), ) ); // ─── Apply Themes ──────────────────────────────────────────────────────────── html { @include mat.all-component-themes($light-theme); // The theme is built with the legacy `define-theme` config, whose // component mixins emit resolved component tokens but never declare the // `--mat-sys-*` system variables. App styles reference those variables // directly, so declare them explicitly for each theme context; without // this every such reference silently resolves to nothing. @include mat.system-level-colors($light-theme); @include mat.system-level-typography($light-theme); @include mat.system-level-elevation($light-theme); @include mat.system-level-shape($light-theme); @include mat.system-level-state($light-theme); // ── Light theme design tokens ───────────────────────────────────────── --app-shell-bg: #eef0f3; --app-rail-bg: #eef0f3; --app-header-bg: #eef0f3; --app-content-bg: #f5f6f8; --app-rail-border: transparent; --app-on-surface: #1d1b1e; --app-hover-overlay: rgba(0, 0, 0, 0.06); --app-search-bg: rgba(255, 255, 255, 0.7); --app-search-border: rgba(0, 0, 0, 0.1); --app-selection-color: #2f7bff; --app-selection-on-color: #eef4ff; --app-selection-surface: color-mix( in srgb, var(--app-selection-color) 14%, transparent ); --app-selection-surface-strong: color-mix( in srgb, var(--app-selection-color) 22%, transparent ); --app-selection-border: color-mix( in srgb, var(--app-selection-color) 52%, transparent ); --app-selection-glow: color-mix( in srgb, var(--app-selection-color) 34%, transparent ); // Watch progress in app chrome (rail cards, catalog capsules, the hero). // Over video the player's fixed `--pc-progress` applies instead. --app-progress-color: var(--app-selection-color); // Live / on-air accent (broadcast indicator) --app-live-color: #d12c2c; // Content area --app-content-shadow: -2px 4px 12px rgba(0, 0, 0, 0.06); // Widget surfaces — no borders, layered backgrounds --app-widget-bg: #ffffff; --app-widget-header-bg: #fafbfc; --app-widget-border: transparent; --app-widget-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 4px 12px rgba(0, 0, 0, 0.04); --app-widget-header-border: rgba(0, 0, 0, 0.06); --app-card-hover-bg: #f0f2f5; --app-separator: rgba(0, 0, 0, 0.14); // Typography — subdued, not pure black --app-heading-color: #1a1d23; --app-body-color: #4a5060; --app-muted-color: #8891a4; --app-eyebrow-color: #7c8495; // Provider accent colors --app-provider-xtream: #2f7bff; --app-provider-stalker: #10b981; --app-provider-m3u: #d97706; // Subscription-expiry chips (dashboard source cards). Text/surface pairs // are picked per theme to keep >= 4.5:1 small-text contrast against the // composited chip background. --app-expiry-warn-text: #8a5300; --app-expiry-warn-surface: color-mix(in srgb, #d97706 16%, transparent); --app-expiry-expired-text: #b3261e; --app-expiry-expired-surface: color-mix(in srgb, #f44336 14%, transparent); // Hero --app-accent-gradient: linear-gradient( 135deg, rgba(47, 123, 255, 0.08) 0%, rgba(92, 214, 255, 0.05) 100% ); --app-hero-surface: rgba(47, 123, 255, 0.04); // Detail / hero call to action. The dark theme's light primary (#f2f4f8 // on #0a0e14) inverts to a dark button on the light theme, so it reads as // the one solid CTA on either page background. --app-cta-bg: #1a1d23; --app-cta-fg: #f5f6f8; --app-cta-hover-bg: #2a2f3a; --app-cta-meta-fg: rgba(245, 246, 248, 0.68); // Star rating chip: a deep amber, since the chip's 12px text needs // 4.5:1 over hero artwork under the light scrim (#a16207 measured 3.4:1). --app-rating-color: #7a4a00; .dark-theme { @include mat.all-component-colors($dark-theme); // Only the color roles differ per theme: the elevation shadows are // the same black in both, so html's declarations already apply here. @include mat.system-level-colors($dark-theme); // ── Dark graphite theme ──────────────────────────────────────────── // Inspired by both Slack (sidebar distinction) and GitButler (data density) // Key principle: clearly distinct surface layers using graphite steps // Darkest → lightest: #0d0f12 → #141720 → #1c2030 → #222840 // App shell & navigation --app-shell-bg: #0d0f12; --app-rail-bg: #111318; --app-header-bg: #111318; --app-content-bg: #161a22; --app-rail-border: transparent; --app-on-surface: #e6e1e5; --app-hover-overlay: rgba(255, 255, 255, 0.08); --app-search-bg: rgba(255, 255, 255, 0.05); --app-search-border: rgba(255, 255, 255, 0.09); --app-selection-color: #78adff; --app-selection-on-color: #071225; --app-selection-surface: color-mix( in srgb, var(--app-selection-color) 18%, transparent ); --app-selection-surface-strong: color-mix( in srgb, var(--app-selection-color) 28%, transparent ); --app-selection-border: color-mix( in srgb, var(--app-selection-color) 62%, transparent ); --app-selection-glow: color-mix( in srgb, var(--app-selection-color) 44%, transparent ); // Re-declared so it resolves against this context's selection colour // rather than inheriting the light value computed on `html`. --app-progress-color: var(--app-selection-color); // Live / on-air accent (broadcast indicator) --app-live-color: #ff8f8f; // Content area --app-content-shadow: -4px 8px 20px rgba(0, 0, 0, 0.18); // Widget surfaces — NO border, surface contrast does the work // Widgets sit on content-bg (#161a22), so widget-bg is notably lighter --app-widget-bg: #1e2330; --app-widget-header-bg: #232840; --app-widget-border: transparent; --app-widget-shadow: none; --app-widget-header-border: rgba(255, 255, 255, 0.05); --app-card-hover-bg: #252b3a; --app-separator: rgba(255, 255, 255, 0.14); // Typography — NOT pure white. Warm graphite-tinted whites --app-heading-color: #d8dce8; // soft off-white, not #fff --app-body-color: #8b93a8; // muted blue-grey --app-muted-color: #5c6478; // dimmed --app-eyebrow-color: #6b7494; // eyebrow labels // Provider accents (slightly brighter for dark bg) --app-provider-xtream: #78adff; --app-provider-stalker: #34d399; --app-provider-m3u: #fbbf24; // Subscription-expiry chips — brighter text for the dark surfaces; // expired matches the --app-live-color accent family. --app-expiry-warn-text: #fbbf24; --app-expiry-warn-surface: color-mix(in srgb, #fbbf24 16%, transparent); --app-expiry-expired-text: #ff8f8f; --app-expiry-expired-surface: color-mix( in srgb, #ff8f8f 16%, transparent ); // Hero section --app-accent-gradient: linear-gradient( 135deg, rgba(120, 173, 255, 0.12) 0%, rgba(92, 214, 255, 0.07) 100% ); --app-hero-surface: rgba(30, 35, 48, 0.9); // Detail / hero call to action (light primary) and rating accent --app-cta-bg: #f2f4f8; --app-cta-fg: #0a0e14; --app-cta-hover-bg: #ffffff; --app-cta-meta-fg: #5a6274; --app-rating-color: #ffcf5c; } } // ─── Global refined outlined inputs ────────────────────────────────────────── // Applied app-wide AFTER mat.all-component-themes so these overrides win the // cascade. Pairs with MAT_FORM_FIELD_DEFAULT_OPTIONS (outline + dynamic) set // in app.config.ts. $app-refined-input-density: mat.define-theme(( density: (scale: -2), )); html { @include mat.form-field-density($app-refined-input-density); // Declared on the field itself (not on html) so the var(--app-*) values // resolve inside .dark-theme instead of being inherited as light values. // The resting and hover outlines keep the theme's outline roles: the app // hairline tokens fall far under the 3:1 boundary contrast a field needs. // Labels use the body token, which clears 4.5:1 in both themes. // Text sizes stay on the theme defaults: this theme leaves the floated // label's size token undeclared, so it inherits the field's text size and // scales it to 75%; a 13px field would float a 10px label. .mat-mdc-form-field { @include mat.form-field-overrides( ( outlined-focus-outline-color: var(--app-selection-color), outlined-container-shape: 10px, outlined-label-text-color: var(--app-body-color), outlined-hover-label-text-color: var(--app-body-color), outlined-focus-label-text-color: var(--app-selection-color), outlined-input-text-color: var(--app-heading-color), outlined-caret-color: var(--app-selection-color), ) ); } // Inputs sit slightly above the surrounding surface. color-mix is inlined // here (not via a custom property on html) so var(--app-widget-bg) // resolves at the wrapper — inside .dark-theme — rather than being // locked to the light-mode value at html-level inheritance. .mat-form-field-appearance-outline .mat-mdc-text-field-wrapper { background: color-mix(in srgb, var(--app-widget-bg) 85%, white 7%); border-radius: 10px; } .mat-mdc-form-field .mat-mdc-input-element, .mat-mdc-form-field .mat-mdc-select-value { font-size: 13.5px; } // ─── Destructive actions ─────────────────────────────────────────────── // Material only emits `.mat-warn` button colors for M2 themes, so the // `warn` color input is a no-op with this M3 theme. Buttons that remove or // discard user data opt in with this class instead; the error tokens are // declared per theme context above, so light and dark each get their own // error/on-error pair. .app-destructive-button { @include mat.button-overrides( ( filled-container-color: var(--mat-sys-error), filled-label-text-color: var(--mat-sys-on-error), filled-state-layer-color: var(--mat-sys-on-error), filled-ripple-color: color-mix(in srgb, var(--mat-sys-on-error) 12%, transparent), text-label-text-color: var(--mat-sys-error), text-state-layer-color: var(--mat-sys-error), text-ripple-color: color-mix(in srgb, var(--mat-sys-error) 12%, transparent), outlined-label-text-color: var(--mat-sys-error), outlined-outline-color: var(--mat-sys-error), outlined-state-layer-color: var(--mat-sys-error), outlined-ripple-color: color-mix(in srgb, var(--mat-sys-error) 12%, transparent), ) ); @include mat.icon-button-overrides( ( icon-color: var(--mat-sys-error), state-layer-color: var(--mat-sys-error), ripple-color: color-mix(in srgb, var(--mat-sys-error) 12%, transparent), ) ); } .mat-mdc-form-field-subscript-wrapper { font-size: 11px; letter-spacing: 0.01em; margin-top: 2px; .mat-mdc-form-field-hint-wrapper, .mat-mdc-form-field-error-wrapper { padding: 0 12px; } .mat-mdc-form-field-hint { color: var(--app-muted-color); } } }