diff --git a/docs/tasks/24-10.md b/docs/tasks/24-10.md
index ccac1ab..ab0d93e 100644
--- a/docs/tasks/24-10.md
+++ b/docs/tasks/24-10.md
@@ -1,7 +1,7 @@
---
id: 24-10
title: 24-10 Redesign Integration Verification
-status: Ready for Dev
+status: QA Review
date: 2026-10-31
parent: 24
---
diff --git a/docs/tasks/manual-tests/24-10.md b/docs/tasks/manual-tests/24-10.md
new file mode 100644
index 0000000..65a4d4d
--- /dev/null
+++ b/docs/tasks/manual-tests/24-10.md
@@ -0,0 +1,41 @@
+# Manual tests — 24-10 Redesign Integration Verification
+
+## Done by the QA pass (2026-10-31)
+
+Automated (`tests/Feature/Design/RedesignIntegrationTest.php`, 6 tests, plus the whole Design directory): for all eight public pages in Arabic
+and English — exactly one h1, alt text on every image, no raw translation key text, no "Unsplash" or "mockup"; login and forgot-password carry the
+auth layer; admin pages (dashboard, pending bookings, posts), client pages (dashboard, booking) and the Nablus form load none of the public assets;
+the booking form on `/` and `/booking` sits inside its own Livewire component; WCAG contrast measured.
+
+In a real browser (Chrome via Playwright, libra.test):
+- Home hero balance level after load; with a real mouse over the frame it tips to about +8.4° and returns to level after leaving.
+- Finale balance −12° at the top, level (≈0°) at the bottom; progress bar scaleX(1) at the bottom; nav gets `scrolled`; no duplicate ids; 44/44 reveals.
+- Five `wire:navigate` hops (About, Articles, Booking, Terms, Home): hero balance still level, reveals work, finale still levels.
+- Reduced motion emulated: 44/44 reveals visible immediately, all three balances `rotate(0 200 110)`.
+- Focus ring on the first nav link: solid 2px gold.
+- Booking from the home card: day strip loads days and slots, selecting a day/slot works, confirm step, Confirm lands on `/booking/success`; one pending guest consultation created.
+- Booking from `/booking`: month calendar, slot, form, confirm, success page; one pending guest consultation created.
+- Both test consultations (`qa-24-10-*@example.invalid`) and the 4 queue jobs they created were deleted; local DB back to 12 consultations / 12 jobs.
+- Metrics sweep at 390/768/1440 in Arabic: no horizontal scroll on the pages listed in the ledger entry; h1 count 1 on public pages.
+
+Contrast (measured): body ink on cream 14.3:1; muted on cream 5.7:1; gold-light on forest 5.6:1; on footer 6.5:1; ink on gold button 4.9:1; olive links 6.5:1;
+gold-ink eyebrow/meta text **3.9:1 / 4.4:1 with the original `#8A7555` — below AA — darkened to `#7A6648`: 4.9:1 on cream, 5.5:1 on white**; footer legal line
+raised from 75% to 90% opacity (4.3 → 5.6:1). Ruling logged.
+
+## Defect found and fixed in this pass
+
+The booking component's template began with a Blade `@if`, so Livewire did not attach its root: on `/` and `/booking` the form fields were not bound to the
+component (console warnings "property does not exist on component pages.home"). The server-side tests passed anyway because they mount the component alone.
+Fixed by giving the template a single root `
`; a test now checks the nearest Livewire root of the form.
+
+## Still for you
+
+- [ ] Look at each page once in Arabic and English at a phone width (both languages were checked by tests, but only Arabic by the width sweep).
+- [ ] Tab through navigation, footer and the booking card with the keyboard: gold ring visible everywhere.
+- [ ] In the admin (`/admin/bookings/pending`) confirm you can see a real test booking if you make one (the two I made are deleted).
+- [ ] Open the admin and client dashboards and the Nablus workshop form: look unchanged.
+
+## Open items for your decision (not fixed — outside the files this task may edit)
+
+- `/login` and `/forgot-password` have no `
` (Flux heading renders a div) and three inputs without a programmatic label: those come from `resources/views/livewire/auth/*`, which tasks 24-9/24-10 forbid editing.
+- Old tests now stale against the redesign (they assert the previous markup/copy): in `HomePageTest`, `AboutPageTest`, `NavigationTest` (language toggle), `ResponsiveDesignTest` (public layout padding), plus failures that were already there before the redesign (footer copyright wording, `AuthBackgroundPatternTest`, focus-visible in `app.css`, admin `role="main"`).
diff --git a/docs/tasks/statuses.md b/docs/tasks/statuses.md
index 9f58dd1..cce2ce7 100644
--- a/docs/tasks/statuses.md
+++ b/docs/tasks/statuses.md
@@ -146,4 +146,4 @@
| 24-7 | 24-7 About Page | QA Review |
| 24-8 | 24-8 Articles and Legal Pages | QA Review |
| 24-9 | 24-9 Auth Pages Type and Controls | QA Review |
-| 24-10 | 24-10 Redesign Integration Verification | Ready for Dev |
+| 24-10 | 24-10 Redesign Integration Verification | QA Review |
diff --git a/resources/css/public.css b/resources/css/public.css
index d3b4659..370280f 100644
--- a/resources/css/public.css
+++ b/resources/css/public.css
@@ -4,7 +4,7 @@
/* ---------- Tokens ---------- */
body.site{
- --forest:#2D3624; --forest-2:#232A1C; --gold:#A68966; --gold-ink:#8A7555; --gold-lt:#C4A882;
+ --forest:#2D3624; --forest-2:#232A1C; --gold:#A68966; --gold-ink:#7A6648; --gold-lt:#C4A882;
--cream:#F4F1EA; --olive:#4A5D23; --ink:#1E2219; --muted:#5B6152; --line:rgba(45,54,36,.14);
--display:"Markazi Text","Times New Roman",serif; --body:"IBM Plex Sans Arabic",system-ui,sans-serif;
--r:10px; --ease:cubic-bezier(.2,.7,.2,1);
@@ -110,7 +110,7 @@ html[dir=ltr] body.site .foot a:hover{transform:translateX(3px)}
body.site .foot .brand b{color:var(--cream)}
body.site .foot .brand small{color:var(--gold-lt)}
body.site .foot p{margin:16px 0 0;max-width:26em}
-body.site .site-footer .legal{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;border-top:1px solid rgba(244,241,234,.1);margin-top:48px;padding-top:22px;font-size:.8rem;color:rgba(196,168,130,.75)}
+body.site .site-footer .legal{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;border-top:1px solid rgba(244,241,234,.1);margin-top:48px;padding-top:22px;font-size:.8rem;color:rgba(196,168,130,.9)}
body.site .foot span{display:block;padding:10px 0}
body.site .foot a.brand{display:flex;padding:0}
body.site .foot .brand span{display:block;padding:0}
diff --git a/resources/views/livewire/pages/booking.blade.php b/resources/views/livewire/pages/booking.blade.php
index fe6a7c5..b3407c5 100644
--- a/resources/views/livewire/pages/booking.blade.php
+++ b/resources/views/livewire/pages/booking.blade.php
@@ -196,6 +196,7 @@ new #[Layout('components.layouts.public')] class extends Component
}
}; ?>
+
@if($compact)
@if($isLoggedIn)
@@ -380,3 +381,4 @@ new #[Layout('components.layouts.public')] class extends Component
@endif
+
diff --git a/tests/Feature/Design/RedesignIntegrationTest.php b/tests/Feature/Design/RedesignIntegrationTest.php
new file mode 100644
index 0000000..a367061
--- /dev/null
+++ b/tests/Feature/Design/RedesignIntegrationTest.php
@@ -0,0 +1,121 @@
+create(['day_of_week' => $dow, 'is_active' => true, 'start_time' => '09:00', 'end_time' => '13:00']);
+ }
+ Post::factory()->published()->count(3)->create();
+ Page::factory()->terms()->create();
+ Page::factory()->create(['slug' => 'privacy', 'title_en' => 'Privacy Policy', 'title_ar' => 'سياسة الخصوصية']);
+});
+
+function publicUrls(): array
+{
+ return ['/', '/about', '/posts', '/posts/'.Post::first()->id, '/booking', '/booking/success', '/page/terms', '/page/privacy'];
+}
+
+test('public pages: one h1, alt text, no raw keys, no mockup text, in both languages', function () {
+ foreach (['ar', 'en'] as $locale) {
+ foreach (publicUrls() as $url) {
+ $html = $this->withSession(['locale' => $locale])->get($url)->assertOk()->getContent();
+
+ expect(substr_count($html, 'toBe(1, "$locale $url h1");
+
+ preg_match_all('/
]*>/', $html, $imgs);
+ foreach ($imgs[0] as $img) {
+ expect($img)->toMatch('/\balt="[^"]+"/', "$locale $url img alt");
+ }
+
+ $text = strip_tags(preg_replace('~<(script|style)\b.*?\1>~s', '', $html));
+ expect(preg_match('/(?toBe(0, "$locale $url raw key: ".($m[0] ?? ''));
+
+ expect($html)->not->toContain('Unsplash')->not->toContain('mockup');
+ expect($html)->toContain('site-progress')->toContain('id="site-nav"');
+ }
+ }
+});
+
+test('auth pages carry the auth layer in both languages', function () {
+ foreach (['ar', 'en'] as $locale) {
+ foreach (['/login', '/forgot-password'] as $url) {
+ $html = $this->withSession(['locale' => $locale])->get($url)->assertOk()->getContent();
+ expect($html)->toContain('site-auth')->not->toContain('Unsplash')->not->toContain('mockup');
+ }
+ }
+});
+
+test('unchanged areas load none of the public assets', function () {
+ $admin = User::factory()->admin()->create();
+ foreach (['/admin/dashboard', '/admin/bookings/pending', '/admin/posts'] as $url) {
+ $html = $this->actingAs($admin)->get($url)->assertOk()->getContent();
+ expect($html)->not->toContain('public.css')->not->toContain('public.js')->not->toContain('class="site')->not->toContain('site-auth');
+ }
+
+ auth()->logout();
+ $client = User::factory()->individual()->create();
+ foreach (['/client/dashboard', '/client/consultations/book'] as $url) {
+ $html = $this->actingAs($client)->get($url)->assertOk()->getContent();
+ expect($html)->not->toContain('public.css')->not->toContain('public.js')->not->toContain('class="site')->not->toContain('site-auth');
+ }
+
+ $nablus = $this->get('/forms/nablus-workshop')->assertOk()->getContent();
+ expect($nablus)->not->toContain('public.css')->not->toContain('class="site');
+});
+
+test('the public interactive controls carry no tabindex trap', function () {
+ $html = $this->get('/')->assertOk()->getContent();
+ expect($html)->not->toMatch('/tabindex="[1-9]/');
+ expect($html)->toContain('aria-label="'.__('site.nav_menu').'"');
+});
+
+test('measured contrast of the redesign colour pairs meets WCAG AA', function () {
+ $lum = function (string $hex) {
+ [$r, $g, $b] = array_map(fn ($i) => hexdec(substr(ltrim($hex, '#'), $i, 2)) / 255, [0, 2, 4]);
+ $f = fn ($c) => $c <= 0.03928 ? $c / 12.92 : (($c + 0.055) / 1.055) ** 2.4;
+
+ return 0.2126 * $f($r) + 0.7152 * $f($g) + 0.0722 * $f($b);
+ };
+ $ratio = function (string $a, string $b) use ($lum) {
+ [$hi, $lo] = $lum($a) >= $lum($b) ? [$lum($a), $lum($b)] : [$lum($b), $lum($a)];
+
+ return ($hi + 0.05) / ($lo + 0.05);
+ };
+
+ expect($ratio('#1E2219', '#F4F1EA'))->toBeGreaterThanOrEqual(4.5); // body text on cream
+ expect($ratio('#C4A882', '#2D3624'))->toBeGreaterThanOrEqual(4.5); // gold-lt small text on forest
+ expect($ratio('#C4A882', '#232A1C'))->toBeGreaterThanOrEqual(4.5); // gold-lt on footer
+ expect($ratio('#1E2219', '#A68966'))->toBeGreaterThanOrEqual(4.5); // text on the gold button
+ expect($ratio('#7A6648', '#F4F1EA'))->toBeGreaterThanOrEqual(4.5); // gold-ink eyebrow on cream
+ expect($ratio('#7A6648', '#FFFFFF'))->toBeGreaterThanOrEqual(4.5); // gold-ink post meta on white
+ expect($ratio('#5B6152', '#F4F1EA'))->toBeGreaterThanOrEqual(4.5); // muted text on cream
+});
+
+function nearestLivewireRoot(string $html, string $xpathTarget): ?string
+{
+ $dom = new DOMDocument;
+ @$dom->loadHTML(''.$html);
+ $xp = new DOMXPath($dom);
+ $node = $xp->query($xpathTarget)->item(0);
+ while ($node instanceof DOMElement) {
+ if ($node->hasAttribute('wire:id')) {
+ return $node->getAttribute('wire:snapshot');
+ }
+ $node = $node->parentNode;
+ }
+
+ return null;
+}
+
+test('the booking form on the home page and /booking sits inside its own Livewire component', function () {
+ $home = $this->get('/')->assertOk()->getContent();
+ expect(nearestLivewireRoot($home, '//form[contains(@class,"book-r")]'))->toContain('guestName');
+
+ $page = $this->get('/booking')->assertOk()->getContent();
+ expect(nearestLivewireRoot($page, '//*[contains(@class,"book-r")]'))->toContain('guestName');
+});