diff --git a/flutter_client/assets/error-copy.json b/flutter_client/assets/error-copy.json
new file mode 100644
index 00000000..a3c3e4cf
--- /dev/null
+++ b/flutter_client/assets/error-copy.json
@@ -0,0 +1,16 @@
+{
+ "unknown": "Something went wrong.",
+ "unauthenticated": "Your session has ended. Please sign in again.",
+ "connection_refused": "Couldn't reach the server. Check the URL and try again.",
+ "lidarr_unreachable": "Lidarr is unreachable right now. Try again, or check Admin → Integrations.",
+ "lidarr_disabled": "Lidarr integration is not enabled.",
+ "lidarr_auth_failed": "Lidarr authentication failed.",
+ "lidarr_defaults_incomplete": "Lidarr is missing a default quality profile or root folder. Set them in Admin → Integrations.",
+ "lidarr_server_error": "Lidarr returned an error. Check Lidarr's logs for the cause.",
+ "lidarr_rejected": "Lidarr rejected the request. Check the server logs for the field-level reason.",
+ "lidarr_album_lookup_failed": "Lidarr doesn't recognize this album. Try Resolve or Delete file instead.",
+ "album_mbid_missing": "This track has no Lidarr album to remove.",
+ "request_not_pending": "This request is no longer pending.",
+ "request_not_found": "That request no longer exists.",
+ "track_not_found": "That track no longer exists."
+}
diff --git a/flutter_client/assets/svg/album-fallback.svg b/flutter_client/assets/svg/album-fallback.svg
new file mode 100644
index 00000000..9720c751
--- /dev/null
+++ b/flutter_client/assets/svg/album-fallback.svg
@@ -0,0 +1,20 @@
+
+
+
diff --git a/flutter_client/lib/theme/tokens.dart b/flutter_client/lib/theme/tokens.dart
new file mode 100644
index 00000000..7468012d
--- /dev/null
+++ b/flutter_client/lib/theme/tokens.dart
@@ -0,0 +1,27 @@
+// GENERATED — do not edit. Source: shared/fabledsword.tokens.json
+// Run `dart run tool/gen_tokens.dart` to regenerate.
+import 'package:flutter/material.dart';
+
+class FabledSwordTokens {
+ static const Color obsidian = Color(0xFF14171A);
+ static const Color iron = Color(0xFF1E2228);
+ static const Color slate = Color(0xFF2C313A);
+ static const Color pewter = Color(0xFF3F4651);
+ static const Color parchment = Color(0xFFE8E4D8);
+ static const Color vellum = Color(0xFFC2BFB4);
+ static const Color ash = Color(0xFF9C9A92);
+ static const Color moss = Color(0xFF4A5D3F);
+ static const Color bronze = Color(0xFF8B7355);
+ static const Color oxblood = Color(0xFF6B2118);
+ static const Color warning = Color(0xFF8B6F1E);
+ static const Color error = Color(0xFFC04A1F);
+ static const Color info = Color(0xFF3D5A6E);
+ static const Color accent = Color(0xFF4A6B5C);
+ static const double radiusSm = 4;
+ static const double radiusMd = 8;
+ static const double radiusLg = 12;
+ static const double radiusXl = 16;
+ static const String fontDisplay = "Fraunces";
+ static const String fontBody = "Inter";
+ static const String fontMono = "JetBrains Mono";
+}
diff --git a/flutter_client/pubspec.yaml b/flutter_client/pubspec.yaml
index 940b162f..30e6378d 100644
--- a/flutter_client/pubspec.yaml
+++ b/flutter_client/pubspec.yaml
@@ -30,10 +30,7 @@ dev_dependencies:
flutter:
uses-material-design: true
- # assets:
- # - assets/svg/
- # - assets/error-copy.json
- # - shared/fabledsword.tokens.json
- # NOTE: Task 5 will sync assets/ + shared/ and uncomment the block above.
- # The flutter test runner refuses to build a bundle with missing asset
- # paths, even though `flutter pub get` and `flutter analyze` only warn.
+ assets:
+ - assets/svg/
+ - assets/error-copy.json
+ - shared/fabledsword.tokens.json
diff --git a/flutter_client/shared/fabledsword.tokens.json b/flutter_client/shared/fabledsword.tokens.json
new file mode 100644
index 00000000..e8fa04ec
--- /dev/null
+++ b/flutter_client/shared/fabledsword.tokens.json
@@ -0,0 +1,34 @@
+{
+ "colors": {
+ "obsidian": "#14171A",
+ "iron": "#1E2228",
+ "slate": "#2C313A",
+ "pewter": "#3F4651",
+ "parchment": "#E8E4D8",
+ "vellum": "#C2BFB4",
+ "ash": "#9C9A92",
+ "moss": "#4A5D3F",
+ "bronze": "#8B7355",
+ "oxblood": "#6B2118",
+ "warning": "#8B6F1E",
+ "error": "#C04A1F",
+ "info": "#3D5A6E",
+ "accent": "#4A6B5C"
+ },
+ "radii": {
+ "sm": "4px",
+ "md": "8px",
+ "lg": "12px",
+ "xl": "16px"
+ },
+ "fonts": {
+ "display": "Fraunces",
+ "body": "Inter",
+ "mono": "JetBrains Mono"
+ },
+ "fontStacks": {
+ "display": "'Fraunces', Georgia, serif",
+ "body": "'Inter', system-ui, sans-serif",
+ "mono": "'JetBrains Mono', ui-monospace, monospace"
+ }
+}
diff --git a/flutter_client/tool/gen_tokens.dart b/flutter_client/tool/gen_tokens.dart
new file mode 100644
index 00000000..4ec37f7a
--- /dev/null
+++ b/flutter_client/tool/gen_tokens.dart
@@ -0,0 +1,42 @@
+// Reads shared/fabledsword.tokens.json and emits lib/theme/tokens.dart.
+// Run via `dart run tool/gen_tokens.dart`. The generated file is
+// committed (CI validates it matches the JSON to catch drift).
+import 'dart:convert';
+import 'dart:io';
+
+void main() {
+ final jsonFile = File('shared/fabledsword.tokens.json');
+ final tokens = jsonDecode(jsonFile.readAsStringSync()) as Map;
+ final colors = (tokens['colors'] as Map).cast();
+ final radii = (tokens['radii'] as Map).cast();
+ final fonts = (tokens['fonts'] as Map).cast();
+
+ final out = StringBuffer()
+ ..writeln('// GENERATED — do not edit. Source: shared/fabledsword.tokens.json')
+ ..writeln('// Run `dart run tool/gen_tokens.dart` to regenerate.')
+ ..writeln("import 'package:flutter/material.dart';")
+ ..writeln()
+ ..writeln('class FabledSwordTokens {');
+
+ colors.forEach((name, hex) {
+ final value = hex.replaceFirst('#', '0xFF');
+ out.writeln(' static const Color ${_camel(name)} = Color($value);');
+ });
+
+ radii.forEach((name, px) {
+ final v = px.replaceAll('px', '');
+ out.writeln(' static const double radius${_pascal(name)} = $v;');
+ });
+
+ fonts.forEach((name, family) {
+ out.writeln(' static const String font${_pascal(name)} = ${jsonEncode(family)};');
+ });
+
+ out.writeln('}');
+
+ File('lib/theme/tokens.dart').writeAsStringSync(out.toString());
+ stdout.writeln('wrote lib/theme/tokens.dart');
+}
+
+String _camel(String s) => s; // tokens are already lowerCamel-friendly
+String _pascal(String s) => s[0].toUpperCase() + s.substring(1);
diff --git a/flutter_client/tool/sync_shared.sh b/flutter_client/tool/sync_shared.sh
new file mode 100755
index 00000000..d1931ac0
--- /dev/null
+++ b/flutter_client/tool/sync_shared.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+# Copies shared assets from web/ into flutter_client/. Run before
+# `flutter build` and as part of CI. Idempotent.
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+
+mkdir -p shared assets/svg
+cp ../web/src/lib/styles/tokens.json shared/fabledsword.tokens.json
+cp ../web/src/lib/styles/error-copy.json assets/error-copy.json
+cp ../web/static/placeholders/album-fallback.svg assets/svg/album-fallback.svg
+
+echo "shared assets synced from web/"