diff --git a/_specs/passphrase-front-page.md b/_specs/passphrase-front-page.md
new file mode 100644
index 0000000..bd13c8e
--- /dev/null
+++ b/_specs/passphrase-front-page.md
@@ -0,0 +1,45 @@
+# Spec for passphrase-front-page
+
+branch: claude/feature/passphrase-front-page
+
+## Summary
+Add a passphrase-gated front page that is the first thing a user sees when they open the application. The page displays an application banner and a passphrase entry field. Only after the correct passphrase is entered does the user proceed to the existing landing page. The `Footer` component is included on this page.
+
+## Functional Requirements
+- The app opens to the new front page instead of the landing page.
+- The front page displays an application banner (app name/logo/title).
+- The front page displays a passphrase input field (type password) and a submit control.
+- When the user submits the correct passphrase (`DororthASL`), the app transitions to the existing landing page.
+- When the user submits an incorrect passphrase, an error message is shown and the input is cleared; the user remains on the front page.
+- The `Footer` component is rendered at the bottom of the front page.
+- The passphrase is not displayed in plain text at any point.
+- The passphrase should not be stored in `localStorage`, `sessionStorage`, or any persistent client-side store — it only needs to gate the current session.
+
+## Possible Edge Cases
+- Submitting an empty passphrase should show an error (not silently fail).
+- The passphrase comparison is case-sensitive (`DororthASL` is the only valid value).
+- Once the user has passed the gate, navigating back (browser back button) should not re-show the front page within the same session.
+- The passphrase should not appear in the page source, git history, or bundle in a way that is trivially discoverable — consider keeping it out of the component render logic if possible (e.g. an env variable or a hashed comparison).
+
+## Acceptance Criteria
+- [ ] Opening the app shows the front page with a banner and passphrase field.
+- [ ] Entering `DororthASL` and submitting advances the user to the landing page.
+- [ ] Entering any other value shows an inline error and clears the input.
+- [ ] Submitting an empty field shows an error.
+- [ ] The `Footer` component is visible on the front page.
+- [ ] The front page is not shown again after a successful passphrase entry within the same session.
+- [ ] The passphrase input masks the entered text.
+
+## Open Questions
+- Should the banner be a new component or a simple styled heading on the front page?
+ - new component
+- Should session-level gating use React state only (resets on page reload) or `sessionStorage` (survives reload within the tab)?
+ - use sessionStorage
+
+## Testing Guidelines
+Create a test file in the `./tests` folder for this feature and create meaningful tests for the following cases:
+- Renders the front page (banner + passphrase input + footer) on initial load.
+- Entering the correct passphrase and submitting transitions to the landing page view.
+- Entering an incorrect passphrase shows an error message and does not advance.
+- Submitting with an empty passphrase shows a validation error.
+- The passphrase input field has `type="password"`.
diff --git a/src/App.css b/src/App.css
index 704c892..1241c7b 100644
--- a/src/App.css
+++ b/src/App.css
@@ -606,3 +606,99 @@
max-width: 100%;
}
}
+
+/* ── App banner ───────────────────────────────────────── */
+
+.app-banner {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 32px;
+}
+
+.app-banner__icon {
+ font-size: 48px;
+ line-height: 1;
+}
+
+.app-banner__title {
+ margin: 0;
+ font-size: clamp(28px, 5vw, 42px);
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ color: var(--text-h);
+}
+
+.app-banner__subtitle {
+ margin: 0;
+ font-size: 16px;
+ color: var(--text);
+}
+
+/* ── Passphrase page ──────────────────────────────────── */
+
+.passphrase-page {
+ min-height: 100svh;
+ display: flex;
+ flex-direction: column;
+ padding: 28px 12px;
+
+ @media (max-width: 640px) {
+ padding: 32px 16px;
+ }
+}
+
+.passphrase-page__body {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.passphrase-page__form {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ width: 100%;
+ max-width: 360px;
+ background: var(--code-bg);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 28px 24px;
+ box-shadow: var(--shadow);
+}
+
+.passphrase-page__label {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--text-h);
+}
+
+.passphrase-page__input {
+ width: 100%;
+ padding: 10px 12px;
+ font-size: 15px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg);
+ color: var(--text-h);
+ box-sizing: border-box;
+
+ &:focus {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ border-color: var(--accent);
+ }
+}
+
+.passphrase-page__error {
+ margin: 0;
+ font-size: 14px;
+ color: var(--color-needs-fix);
+}
+
+.passphrase-page__submit {
+ align-self: flex-end;
+}
diff --git a/src/App.jsx b/src/App.jsx
index 8a4c743..f186d51 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -3,11 +3,12 @@ import { CardColors } from './data/card-colors'
import { LandingPage } from './components/LandingPage'
import { FlashcardSession } from './components/FlashcardSession'
import { Footer } from './components/Footer'
+import { PassphrasePage } from './components/PassphrasePage'
import './App.css'
import {Toaster} from "react-hot-toast";
function App() {
- const [view, setView] = useState('input')
+ const [view, setView] = useState(() => sessionStorage.getItem('asl-unlocked') ? 'input' : 'gate')
const [terms, setTerms] = useState([])
const [cardColors, setCardColors] = useState([])
const [categoryTitle, setCategoryTitle] = useState('')
@@ -27,6 +28,15 @@ function App() {
setView('input')
}
+ function handleUnlock() {
+ sessionStorage.setItem('asl-unlocked', '1')
+ setView('input')
+ }
+
+ if (view === 'gate') {
+ return
American Sign Language practice made easy
+