diff --git a/services/web-ui/public/screens-auth.jsx b/services/web-ui/public/screens-auth.jsx
new file mode 100644
index 0000000..349330d
--- /dev/null
+++ b/services/web-ui/public/screens-auth.jsx
@@ -0,0 +1,179 @@
+// LoginScreen + SetupScreen — layout B from the auth brainstorm spec:
+// 22px wordmark + "WILD DRAGON BROADCAST" tagline above a --bg-1 card.
+// Matches DESIGN.md tokens; no decoration, dense, ops register.
+
+(function () {
+ const API_BASE = '/api/v1';
+
+ async function postJson(path, body) {
+ return fetch(API_BASE + path, {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Requested-With': 'dragonflight-ui',
+ },
+ body: JSON.stringify(body),
+ });
+ }
+
+ function Brand() {
+ return (
+
+
Dragonflight
+
+ Wild Dragon Broadcast
+
+
+ );
+ }
+
+ function Card({ children }) {
+ return (
+ {children}
+ );
+ }
+
+ function Field({ label, type = 'text', value, onChange, autoComplete, autoFocus }) {
+ return (
+
+
+ onChange(e.target.value)}
+ style={{
+ width: '100%',
+ background: 'var(--bg-3)',
+ border: '1px solid var(--border)',
+ borderRadius: 4,
+ padding: '8px 10px',
+ fontSize: 12.5,
+ color: 'var(--text-1)',
+ fontFamily: 'var(--font-mono)',
+ boxSizing: 'border-box',
+ }}
+ />
+
+ );
+ }
+
+ function Button({ children, disabled, onClick, type = 'button' }) {
+ return (
+
+ );
+ }
+
+ function ErrorRow({ text }) {
+ if (!text) return null;
+ return (
+ {text}
+ );
+ }
+
+ function Screen({ children }) {
+ return (
+
+
+
+ );
+ }
+
+ function LoginScreen({ onDone }) {
+ const [username, setUsername] = React.useState('');
+ const [password, setPassword] = React.useState('');
+ const [error, setError] = React.useState('');
+ const [busy, setBusy] = React.useState(false);
+ const submit = async () => {
+ setError(''); setBusy(true);
+ try {
+ const r = await postJson('/auth/login', { username, password });
+ if (r.status === 200) { onDone(); return; }
+ const body = await r.json().catch(() => ({}));
+ setError(body.error || ('Login failed: ' + r.status));
+ } catch (e) { setError(e.message || 'Login failed'); }
+ finally { setBusy(false); }
+ };
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ function SetupScreen({ onDone }) {
+ const [username, setUsername] = React.useState('');
+ const [password, setPassword] = React.useState('');
+ const [confirm, setConfirm] = React.useState('');
+ const [error, setError] = React.useState('');
+ const [busy, setBusy] = React.useState(false);
+ const submit = async () => {
+ setError('');
+ if (password !== confirm) { setError('Passwords do not match'); return; }
+ if (password.length < 12) { setError('Password must be at least 12 characters'); return; }
+ setBusy(true);
+ try {
+ const r = await postJson('/auth/setup', { username, password });
+ if (r.status === 200) { onDone(); return; }
+ const body = await r.json().catch(() => ({}));
+ setError(body.error || ('Setup failed: ' + r.status));
+ } catch (e) { setError(e.message || 'Setup failed'); }
+ finally { setBusy(false); }
+ };
+ return (
+
+
+ First-run setup — create the first admin
+
+
+
+
+
+
+
+ );
+ }
+
+ window.LoginScreen = LoginScreen;
+ window.SetupScreen = SetupScreen;
+})();