Language Learning App
+Create an account
+In development, the link is printed to the API server logs.
+ Sign in +diff --git a/frontend/src/app.css b/frontend/src/app.css
index 3aa20f5..38214a2 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -7,153 +7,153 @@
-------------------------------------------------------------------------- */
@font-face {
- font-family: archivo; /* set name */
- src: url('/fonts/Archivo-Medium.woff2'); /* url of the font */
- font-weight: 500;
+ font-family: archivo; /* set name */
+ src: url('/fonts/Archivo-Medium.woff2'); /* url of the font */
+ font-weight: 500;
}
@font-face {
- font-family: archivo; /* set name */
- src: url('/fonts/Archivo-Regular.woff2'); /* url of the font */
- font-weight: normal;
+ font-family: archivo; /* set name */
+ src: url('/fonts/Archivo-Regular.woff2'); /* url of the font */
+ font-weight: normal;
}
-
/* --------------------------------------------------------------------------
Design Tokens
-------------------------------------------------------------------------- */
:root {
+ /* --- Color: Primary --- */
+ --color-primary: #516356;
+ --color-on-primary: #e9fded;
+ --color-primary-container: #c8d8c8;
+ --color-on-primary-container: #2f342e;
- /* --- Color: Primary --- */
- --color-primary: #516356;
- --color-on-primary: #e9fded;
- --color-primary-container: #c8d8c8;
- --color-on-primary-container: #2f342e;
+ /* --- Color: Secondary --- */
+ --color-secondary-container: #dde4de;
+ --color-on-secondary-container: #2f342e;
- /* --- Color: Secondary --- */
- --color-secondary-container: #dde4de;
- --color-on-secondary-container: #2f342e;
+ /* --- Color: Surface Hierarchy (light → dark = elevated → recessed) --- */
+ --color-surface-container-lowest: #ffffff; /* most elevated */
+ --color-surface-container-low: #f4f4ef;
+ --color-surface: #faf9f5; /* base */
+ --color-surface-container: #eeede9;
+ --color-surface-container-high: #e8e8e3;
+ --color-surface-container-highest: #e2e1dd;
+ --color-surface-dim: #d6dcd2; /* recessed utility */
- /* --- Color: Surface Hierarchy (light → dark = elevated → recessed) --- */
- --color-surface-container-lowest: #ffffff; /* most elevated */
- --color-surface-container-low: #f4f4ef;
- --color-surface: #faf9f5; /* base */
- --color-surface-container: #eeede9;
- --color-surface-container-high: #e8e8e3;
- --color-surface-container-highest:#e2e1dd;
- --color-surface-dim: #d6dcd2; /* recessed utility */
+ /* --- Color: On-Surface --- */
+ --color-on-surface: #2f342e; /* replaces pure black */
+ --color-on-surface-variant: #5c605b;
- /* --- Color: On-Surface --- */
- --color-on-surface: #2f342e; /* replaces pure black */
- --color-on-surface-variant: #5c605b;
+ /* --- Color: Outline --- */
+ --color-outline: #8c908b;
+ --color-outline-variant: #afb3ac; /* use at 20% opacity as "ghost border" */
- /* --- Color: Outline --- */
- --color-outline: #8c908b;
- --color-outline-variant: #afb3ac; /* use at 20% opacity as "ghost border" */
+ /* --- Typography: Font Families --- */
+ --font-display: 'archivo', sans-serif;
+ --font-body: 'Newsreader', serif;
+ --font-label: 'Inter', sans-serif;
- /* --- Typography: Font Families --- */
- --font-display: 'archivo', sans-serif;
- --font-body: 'Newsreader', serif;
- --font-label: 'Inter', sans-serif;
+ /* --- Typography: Scale --- */
+ --text-display-lg: 3.5rem; /* article titles */
+ --text-display-md: 2.75rem;
+ --text-display-sm: 2.25rem;
+ --text-headline-lg: 1.875rem;
+ --text-headline-md: 1.5rem;
+ --text-headline-sm: 1.25rem;
+ --text-title-lg: 1.125rem;
+ --text-title-md: 1rem;
+ --text-body-xl: 1.25rem; /* long-form reading standard */
+ --text-body-lg: 1rem;
+ --text-body-md: 0.9375rem;
+ --text-body-sm: 0.875rem;
+ --text-label-lg: 0.875rem;
+ --text-label-md: 0.75rem; /* metadata, all-caps */
+ --text-label-sm: 0.6875rem;
- /* --- Typography: Scale --- */
- --text-display-lg: 3.5rem; /* article titles */
- --text-display-md: 2.75rem;
- --text-display-sm: 2.25rem;
- --text-headline-lg: 1.875rem;
- --text-headline-md: 1.5rem;
- --text-headline-sm: 1.25rem;
- --text-title-lg: 1.125rem;
- --text-title-md: 1rem;
- --text-body-xl: 1.25rem; /* long-form reading standard */
- --text-body-lg: 1rem;
- --text-body-md: 0.9375rem;
- --text-body-sm: 0.875rem;
- --text-label-lg: 0.875rem;
- --text-label-md: 0.75rem; /* metadata, all-caps */
- --text-label-sm: 0.6875rem;
+ /* --- Typography: Weights --- */
+ --weight-light: 300;
+ --weight-regular: 400;
+ --weight-medium: 500;
+ --weight-semibold: 600;
+ --weight-bold: 700;
- /* --- Typography: Weights --- */
- --weight-light: 300;
- --weight-regular: 400;
- --weight-medium: 500;
- --weight-semibold: 600;
- --weight-bold: 700;
+ /* --- Typography: Line Height --- */
+ --leading-tight: 1.2;
+ --leading-snug: 1.375;
+ --leading-normal: 1.5;
+ --leading-relaxed: 1.6; /* "Digital Paper" body text minimum */
+ --leading-loose: 1.8;
- /* --- Typography: Line Height --- */
- --leading-tight: 1.2;
- --leading-snug: 1.375;
- --leading-normal: 1.5;
- --leading-relaxed: 1.6; /* "Digital Paper" body text minimum */
- --leading-loose: 1.8;
+ /* --- Typography: Letter Spacing --- */
+ --tracking-tight: -0.025em;
+ --tracking-normal: 0em;
+ --tracking-wide: 0.05rem; /* label-md metadata */
+ --tracking-wider: 0.08rem;
- /* --- Typography: Letter Spacing --- */
- --tracking-tight: -0.025em;
- --tracking-normal: 0em;
- --tracking-wide: 0.05rem; /* label-md metadata */
- --tracking-wider: 0.08rem;
+ /* --- Spacing Scale (base-4) --- */
+ --space-1: 0.25rem; /* 4px */
+ --space-2: 0.5rem; /* 8px */
+ --space-3: 1rem; /* 16px — list item separation */
+ --space-4: 1.4rem; /* ~22px — list item group separation */
+ --space-5: 1.75rem; /* 28px */
+ --space-6: 2rem; /* 32px */
+ --space-8: 3rem; /* 48px */
+ --space-10: 4rem; /* 64px */
+ --space-12: 4.5rem; /* 72px */
+ --space-16: 5.5rem; /* 88px — top-of-page breathability */
- /* --- Spacing Scale (base-4) --- */
- --space-1: 0.25rem; /* 4px */
- --space-2: 0.5rem; /* 8px */
- --space-3: 1rem; /* 16px — list item separation */
- --space-4: 1.4rem; /* ~22px — list item group separation */
- --space-5: 1.75rem; /* 28px */
- --space-6: 2rem; /* 32px */
- --space-8: 3rem; /* 48px */
- --space-10: 4rem; /* 64px */
- --space-12: 4.5rem; /* 72px */
- --space-16: 5.5rem; /* 88px — top-of-page breathability */
+ /* --- Border Radius --- */
+ --radius-xs: 0.125rem;
+ --radius-sm: 0.25rem;
+ --radius-md: 0.375rem; /* primary button */
+ --radius-lg: 0.75rem;
+ --radius-xl: 1.25rem;
+ --radius-full: 9999px;
- /* --- Border Radius --- */
- --radius-xs: 0.125rem;
- --radius-sm: 0.25rem;
- --radius-md: 0.375rem; /* primary button */
- --radius-lg: 0.75rem;
- --radius-xl: 1.25rem;
- --radius-full: 9999px;
+ /* --- Elevation: Ambient "Tonal Shadow" --- */
+ --shadow-tonal-sm: 0 4px 16px 0 color-mix(in srgb, var(--color-on-surface) 5%, transparent);
+ --shadow-tonal-md: 0 8px 32px 0 color-mix(in srgb, var(--color-on-surface) 5%, transparent);
- /* --- Elevation: Ambient "Tonal Shadow" --- */
- --shadow-tonal-sm: 0 4px 16px 0 color-mix(in srgb, var(--color-on-surface) 5%, transparent);
- --shadow-tonal-md: 0 8px 32px 0 color-mix(in srgb, var(--color-on-surface) 5%, transparent);
+ /* --- Motion --- */
+ --duration-fast: 100ms;
+ --duration-normal: 200ms;
+ --duration-slow: 400ms;
+ --ease-standard: cubic-bezier(0.2, 0, 0, 1);
- /* --- Motion --- */
- --duration-fast: 100ms;
- --duration-normal: 200ms;
- --duration-slow: 400ms;
- --ease-standard: cubic-bezier(0.2, 0, 0, 1);
-
- /* --- Glass / Frosted Effect --- */
- --glass-bg: color-mix(in srgb, var(--color-surface) 80%, transparent);
- --glass-blur: 24px;
+ /* --- Glass / Frosted Effect --- */
+ --glass-bg: color-mix(in srgb, var(--color-surface) 80%, transparent);
+ --glass-blur: 24px;
}
/* --------------------------------------------------------------------------
Reset & Base
-------------------------------------------------------------------------- */
-*, *::before, *::after {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
}
html {
- font-size: 16px;
- -webkit-text-size-adjust: 100%;
- scroll-behavior: smooth;
+ font-size: 16px;
+ -webkit-text-size-adjust: 100%;
+ scroll-behavior: smooth;
}
body {
- font-family: var(--font-body);
- font-size: var(--text-body-lg);
- font-weight: var(--weight-regular);
- line-height: var(--leading-relaxed);
- color: var(--color-on-surface);
- background-color: var(--color-surface);
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
+ font-family: var(--font-body);
+ font-size: var(--text-body-lg);
+ font-weight: var(--weight-regular);
+ line-height: var(--leading-relaxed);
+ color: var(--color-on-surface);
+ background-color: var(--color-surface);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
}
/* --------------------------------------------------------------------------
@@ -161,37 +161,37 @@ body {
-------------------------------------------------------------------------- */
.display-lg {
- font-family: var(--font-display);
- font-size: var(--text-display-lg);
- font-weight: var(--weight-bold);
- line-height: var(--leading-tight);
- letter-spacing: var(--tracking-tight);
+ font-family: var(--font-display);
+ font-size: var(--text-display-lg);
+ font-weight: var(--weight-bold);
+ line-height: var(--leading-tight);
+ letter-spacing: var(--tracking-tight);
}
.headline-md {
- font-family: var(--font-display);
- font-size: var(--text-headline-md);
- font-weight: var(--weight-semibold);
- line-height: var(--leading-snug);
+ font-family: var(--font-display);
+ font-size: var(--text-headline-md);
+ font-weight: var(--weight-semibold);
+ line-height: var(--leading-snug);
}
.label-md {
- font-family: var(--font-label);
- font-size: var(--text-label-md);
- font-weight: var(--weight-medium);
- letter-spacing: var(--tracking-wide);
- text-transform: uppercase;
+ font-family: var(--font-label);
+ font-size: var(--text-label-md);
+ font-weight: var(--weight-medium);
+ letter-spacing: var(--tracking-wide);
+ text-transform: uppercase;
}
.link {
- color: var(--color-primary);
- text-decoration: none;
- font-weight: var(--weight-medium);
- transition: opacity var(--duration-fast) var(--ease-standard);
+ color: var(--color-primary);
+ text-decoration: none;
+ font-weight: var(--weight-medium);
+ transition: opacity var(--duration-fast) var(--ease-standard);
}
.link:hover {
- opacity: 0.7;
+ opacity: 0.7;
}
/* --------------------------------------------------------------------------
@@ -199,41 +199,41 @@ body {
-------------------------------------------------------------------------- */
.form {
- display: flex;
- flex-direction: column;
- gap: var(--space-4);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-4);
}
.form-header {
- margin-bottom: var(--space-6);
+ margin-bottom: var(--space-6);
}
.form-eyebrow {
- font-family: var(--font-label);
- font-size: var(--text-label-md);
- font-weight: var(--weight-medium);
- letter-spacing: var(--tracking-wide);
- text-transform: uppercase;
- color: var(--color-on-surface-variant);
- margin-bottom: var(--space-1);
+ font-family: var(--font-label);
+ font-size: var(--text-label-md);
+ font-weight: var(--weight-medium);
+ letter-spacing: var(--tracking-wide);
+ text-transform: uppercase;
+ color: var(--color-on-surface-variant);
+ margin-bottom: var(--space-1);
}
.form-title {
- font-family: var(--font-display);
- font-size: var(--text-headline-lg);
- font-weight: var(--weight-semibold);
- line-height: var(--leading-snug);
- color: var(--color-on-surface);
- }
+ font-family: var(--font-display);
+ font-size: var(--text-headline-lg);
+ font-weight: var(--weight-semibold);
+ line-height: var(--leading-snug);
+ color: var(--color-on-surface);
+}
.form-footer {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- margin-top: var(--space-6);
- font-family: var(--font-label);
- font-size: var(--text-body-sm);
- color: var(--color-on-surface-variant);
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ margin-top: var(--space-6);
+ font-family: var(--font-label);
+ font-size: var(--text-body-sm);
+ color: var(--color-on-surface-variant);
}
/* --------------------------------------------------------------------------
@@ -241,42 +241,42 @@ body {
-------------------------------------------------------------------------- */
.btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: var(--space-2);
- padding: var(--space-2) var(--space-4);
- border: none;
- border-radius: var(--radius-md);
- font-family: var(--font-display);
- font-size: var(--text-label-lg);
- font-weight: var(--weight-medium);
- letter-spacing: var(--tracking-wide);
- cursor: pointer;
- text-decoration: none;
- transition:
- opacity var(--duration-normal) var(--ease-standard),
- background-color var(--duration-normal) var(--ease-standard);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-2);
+ padding: var(--space-2) var(--space-4);
+ border: none;
+ border-radius: var(--radius-md);
+ font-family: var(--font-display);
+ font-size: var(--text-label-lg);
+ font-weight: var(--weight-medium);
+ letter-spacing: var(--tracking-wide);
+ cursor: pointer;
+ text-decoration: none;
+ transition:
+ opacity var(--duration-normal) var(--ease-standard),
+ background-color var(--duration-normal) var(--ease-standard);
}
.btn-primary {
- background-color: var(--color-primary);
- color: var(--color-on-primary);
+ background-color: var(--color-primary);
+ color: var(--color-on-primary);
}
.btn-primary:hover {
- opacity: 0.88;
+ opacity: 0.88;
}
.btn-secondary {
- background-color: var(--color-secondary-container);
- color: var(--color-on-secondary-container);
+ background-color: var(--color-secondary-container);
+ color: var(--color-on-secondary-container);
}
.btn-ghost {
- background: none;
- color: var(--color-primary);
- padding-inline: var(--space-2);
+ background: none;
+ color: var(--color-primary);
+ padding-inline: var(--space-2);
}
/* --------------------------------------------------------------------------
@@ -284,97 +284,107 @@ body {
-------------------------------------------------------------------------- */
.field {
- display: flex;
- flex-direction: column;
- gap: var(--space-1);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
}
.field-label {
- font-family: var(--font-label);
- font-size: var(--text-label-md);
- font-weight: var(--weight-medium);
- letter-spacing: var(--tracking-wide);
- text-transform: uppercase;
- color: var(--color-on-surface-variant);
+ font-family: var(--font-label);
+ font-size: var(--text-label-md);
+ font-weight: var(--weight-medium);
+ letter-spacing: var(--tracking-wide);
+ text-transform: uppercase;
+ color: var(--color-on-surface-variant);
}
.field-input {
- background-color: var(--color-surface-container-high);
- color: var(--color-on-surface);
- border: none;
- border-bottom: 1px solid color-mix(in srgb, var(--color-outline-variant) 20%, transparent);
- border-radius: var(--radius-sm) var(--radius-sm) 0 0;
- padding: var(--space-2) var(--space-3);
- font-family: var(--font-body);
- font-size: var(--text-body-lg);
- line-height: var(--leading-normal);
- outline: none;
- width: 100%;
- transition: border-color var(--duration-fast) var(--ease-standard);
+ background-color: var(--color-surface-container-high);
+ color: var(--color-on-surface);
+ border: none;
+ border-bottom: 1px solid color-mix(in srgb, var(--color-outline-variant) 20%, transparent);
+ border-radius: var(--radius-sm) var(--radius-sm) 0 0;
+ padding: var(--space-2) var(--space-3);
+ font-family: var(--font-body);
+ font-size: var(--text-body-lg);
+ line-height: var(--leading-normal);
+ outline: none;
+ width: 100%;
+ transition: border-color var(--duration-fast) var(--ease-standard);
+}
+
+.field-disabled {
+ font-family: var(--font-body);
+ font-size: var(--text-body-lg);
+ color: var(--color-on-surface-variant);
+ padding: var(--space-2) var(--space-3);
+ background-color: var(--color-surface-container);
+ border-radius: var(--radius-sm) var(--radius-sm) 0 0;
+ border-bottom: 1px solid color-mix(in srgb, var(--color-outline-variant) 20%, transparent);
}
.field-input::placeholder {
- color: var(--color-outline-variant);
+ color: var(--color-outline-variant);
}
.field-input:focus {
- border-bottom: 2px solid var(--color-primary);
+ border-bottom: 2px solid var(--color-primary);
}
.field-select {
- background-color: var(--color-surface-container-high);
- color: var(--color-on-surface);
- border: none;
- border-bottom: 1px solid color-mix(in srgb, var(--color-outline-variant) 20%, transparent);
- border-radius: var(--radius-sm) var(--radius-sm) 0 0;
- padding: var(--space-2) var(--space-8) var(--space-2) var(--space-3);
- font-family: var(--font-body);
- font-size: var(--text-body-lg);
- line-height: var(--leading-normal);
- outline: none;
- width: 100%;
- cursor: pointer;
- appearance: none;
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%235c605b' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
- background-repeat: no-repeat;
- background-position: right var(--space-3) center;
- transition: border-color var(--duration-fast) var(--ease-standard);
+ background-color: var(--color-surface-container-high);
+ color: var(--color-on-surface);
+ border: none;
+ border-bottom: 1px solid color-mix(in srgb, var(--color-outline-variant) 20%, transparent);
+ border-radius: var(--radius-sm) var(--radius-sm) 0 0;
+ padding: var(--space-2) var(--space-8) var(--space-2) var(--space-3);
+ font-family: var(--font-body);
+ font-size: var(--text-body-lg);
+ line-height: var(--leading-normal);
+ outline: none;
+ width: 100%;
+ cursor: pointer;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%235c605b' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right var(--space-3) center;
+ transition: border-color var(--duration-fast) var(--ease-standard);
}
.field-select:focus {
- border-bottom: 2px solid var(--color-primary);
+ border-bottom: 2px solid var(--color-primary);
}
.field-textarea {
- background-color: var(--color-surface-container-high);
- color: var(--color-on-surface);
- border: none;
- border-bottom: 1px solid color-mix(in srgb, var(--color-outline-variant) 20%, transparent);
- border-radius: var(--radius-sm) var(--radius-sm) 0 0;
- padding: var(--space-2) var(--space-3);
- font-family: var(--font-body);
- font-size: var(--text-body-lg);
- line-height: var(--leading-relaxed);
- outline: none;
- width: 100%;
- min-height: 14rem;
- resize: vertical;
- transition: border-color var(--duration-fast) var(--ease-standard);
+ background-color: var(--color-surface-container-high);
+ color: var(--color-on-surface);
+ border: none;
+ border-bottom: 1px solid color-mix(in srgb, var(--color-outline-variant) 20%, transparent);
+ border-radius: var(--radius-sm) var(--radius-sm) 0 0;
+ padding: var(--space-2) var(--space-3);
+ font-family: var(--font-body);
+ font-size: var(--text-body-lg);
+ line-height: var(--leading-relaxed);
+ outline: none;
+ width: 100%;
+ min-height: 14rem;
+ resize: vertical;
+ transition: border-color var(--duration-fast) var(--ease-standard);
}
.field-textarea::placeholder {
- color: var(--color-outline-variant);
+ color: var(--color-outline-variant);
}
.field-textarea:focus {
- border-bottom: 2px solid var(--color-primary);
+ border-bottom: 2px solid var(--color-primary);
}
.field-hint {
- font-family: var(--font-label);
- font-size: var(--text-label-md);
- color: var(--color-on-surface-variant);
- margin-top: var(--space-1);
+ font-family: var(--font-label);
+ font-size: var(--text-label-md);
+ color: var(--color-on-surface-variant);
+ margin-top: var(--space-1);
}
/* --------------------------------------------------------------------------
@@ -382,14 +392,14 @@ body {
-------------------------------------------------------------------------- */
.alert {
- padding: var(--space-3);
- border-radius: var(--radius-md);
- font-family: var(--font-label);
- font-size: var(--text-body-sm);
+ padding: var(--space-3);
+ border-radius: var(--radius-md);
+ font-family: var(--font-label);
+ font-size: var(--text-body-sm);
}
.alert-error {
- background-color: color-mix(in srgb, #b3261e 10%, var(--color-surface));
- color: #b3261e;
- border-left: 3px solid #b3261e;
+ background-color: color-mix(in srgb, #b3261e 10%, var(--color-surface));
+ color: #b3261e;
+ border-left: 3px solid #b3261e;
}
diff --git a/frontend/src/routes/app/+layout.server.ts b/frontend/src/routes/app/+layout.server.ts
new file mode 100644
index 0000000..b698d8e
--- /dev/null
+++ b/frontend/src/routes/app/+layout.server.ts
@@ -0,0 +1,36 @@
+import { redirect, type ServerLoad } from '@sveltejs/kit';
+import { getAccountStatusApiAccountStatusGet } from '../../client/sdk.gen.ts';
+import type { AccountStatusResponse } from '../../client/types.gen.ts';
+
+const PROBLEM_FLAGS_COOKIE_NAME = 'account_problem_flags';
+export const load: ServerLoad = async ({ locals, url, cookies }) => {
+ const cachedFlags = cookies.get(PROBLEM_FLAGS_COOKIE_NAME);
+ let problemFlags: string[] = [];
+
+ if (cachedFlags === undefined) {
+ const { data } = await getAccountStatusApiAccountStatusGet({
+ headers: { Authorization: `Bearer ${locals.authToken}` }
+ });
+
+ const flags = (data as AccountStatusResponse)?.problem_flags ?? [];
+
+ // 15-minute cookie-based cache
+ cookies.set(PROBLEM_FLAGS_COOKIE_NAME, JSON.stringify(flags), {
+ path: '/',
+ maxAge: 60 * 15
+ });
+
+ problemFlags = flags;
+ }
+
+ // Redirect to onboarding if not yet completed — guard against redirect loop
+ if (problemFlags.includes('no_onboarding') && !url.pathname.startsWith('/app/onboarding')) {
+ redirect(307, '/app/onboarding');
+ }
+
+ // Don't hard-block on unverified email yet (login gate is not active),
+ // but surface the flag so the layout can show a warning banner.
+ return {
+ emailUnverified: problemFlags.includes('unvalidated_email')
+ };
+};
diff --git a/frontend/src/routes/app/+layout.svelte b/frontend/src/routes/app/+layout.svelte
index 34ec324..50cd343 100644
--- a/frontend/src/routes/app/+layout.svelte
+++ b/frontend/src/routes/app/+layout.svelte
@@ -1,9 +1,31 @@
Getting started
+Tell us a bit about yourself and what you'd like to learn.
+Account
Profile settings coming soon.
+ {#if form?.success} +Language Learning App
+In development, the link is printed to the API server logs.
+ Sign in +You can now sign in.
+ Sign in → + {:else} +