# web3p1 — merge decision record

Files: everything under `public/`, `src/`, `templates/`, `scripts/`. This
directory is a copy of `~/web2` (plain PHP, no build step, no Composer, no
Node) with `/var/www/shtml/web3.1`'s visual design system merged in.
Neither source project was touched — both remain untouched read-only
references. See `CLAUDE.md` for the full architecture description of what
was already here before this merge.

---

## 0. Why this exists

Two sibling projects independently built a website for the same person
(Dr. Evangelos M. Rokas, neurosurgeon, Athens): `web3.1`, a static
dependency-free mockup with a disciplined, well-documented design system
but placeholder content; and `web2`, a working PHP app with a real,
largely-complete bilingual content/data model but a comparatively thin
visual layer. The user's call: web3.1's design system is the visual
basis, web2's backend/data model is the structural basis. This directory
is where that merge actually happened, phase by phase, each phase
committed and tagged (`git log --oneline`, `git tag`) so any step can be
inspected or reverted independently.

## 1. Placement: directly inside the docroot, not beside ~/web2

The user chose `/var/www/shtml/web3p1` — a new directory, sibling to
`web3.1`, inside the same docroot — over continuing in place in `~/web2`
or picking a path outside the docroot. That placement has a real
consequence `~/web2` never had to deal with: without protection,
`src/*.php`, `templates/*.php`, and `scripts/*.php` become directly
web-requestable, since nothing here lives outside the docroot the way
`~/web2` does. Fixed with `Require all denied` `.htaccess` files in
`src/`, `templates/`, and `scripts/` (Phase 0) — the same "only `public/`
is web-facing" boundary `~/web2` gets for free by its location, made
explicit here instead. A symlink at `/var/www/shtml/mweb/web3p1` →
`web3p1/public` mirrors the existing `mweb/web2` pattern, for real
Apache+PHP-FPM browser testing.

## 2. Token system: borrow the discipline, not the hues

web3.1's `tokens.css` has a full space/radius/shadow/blur/motion/opacity/
z-index scale; web2's had only color+type, and every component file
hardcoded the rest. Ported the scales as-is where they were genuinely
additive, but **kept web2's own palette** (Navy/Navy-mid/Teal/Oxblood/
Brass) rather than importing web3.1's Ocean/Oxblood/Verdigris — per the
user's framing, web2 wins on content/substance, and the palette is
substance here, not just styling.

Two things were retuned rather than copied blind, because copying blind
would have been wrong:

- **z-index.** web2 already had real, live hardcoded values (nav header
  100, nav panel 95, nav scrim 90, consent banner 200). Importing
  web3.1's own scale (`--z-thread:20 / --z-header:60 / --z-mega:55 /
  --z-drawer:80 / --z-overlay:90`) unmodified would have collided with
  those. Fitted new named tokens into the real gaps instead
  (`--z-thread:20` turned out to fit cleanly below everything else; the
  rest just got names for values that already existed).
- **Shadow/thread-idle color bases.** Recomputed from `--color-bone-ink`'s
  own rgb(20,26,33), not copied from web3.1's ocean-950 rgb(4,16,30) —
  close in character (both dark blue-black) but not the same palette, so
  using web2's own ink color is what actually sits correctly against it.

The fluid `clamp()` type-scale **mechanism** was kept (web2's existing
approach, not web3.1's fixed-step + `@media` override) to avoid a rename
sweep across every file already referencing `--fs-*` tokens; only the
display-step bounds were retuned toward web3.1's larger hero sizing.
This is an approximation, not a measurement — flagged for a visual check
on the real deployment, not treated as settled.

## 3. Nav: a re-skin, not a rebuild

The original plan assumed web2's nav might need new logic to support
web3.1's 2-level mega-menu pattern against web2's real 3-level taxonomy.
Reading `nav.js` directly disproved that before any code was touched:
`positionFlyout()`, `closeSiblingsAtSameLevel()`, and the enter/leave
listener binding are all already depth-agnostic, built for web2's own
taxonomy (which has two genuinely 3-deep branches) and already correct.
Phase 2 changed only CSS **values** in `nav.css` — shadows, durations,
easing curves, spacing, z-index — onto web2's existing selectors. No
markup, no JS, no bare web3.1 class name (`.mega`, `.burger`, `.nav-link`)
was introduced anywhere.

One real addition: the Information branch (7 children) gets a 2-column
grid; Doctor/Activities (3 children each) stay single-column — a
content-count decision, not a uniform one, or the small branches would
read as sparse in a mega-panel treatment built for 7 items. Implemented
with `.site-nav__panel--depth-1:has(> .site-nav__list--depth-1 >
.site-nav__item:nth-child(5))` — deliberately content-count-based rather
than a branch-order `nth-child` guess (which would break silently if
`NavData`'s branch order ever changed) — and scoped with a direct-child
combinator so it counts only a branch's own immediate items, not items
nested one level deeper inside a depth-2 sub-submenu. Degrades to the
existing single-column layout in any browser without `:has()` support,
which is exactly what the 3-item branches want anyway.

## 4. Neural Thread: home.php and cv.php, nothing else

web3.1's Thread was built for one long homepage with `data-chapter`
sections 1:1 with its nodes. web2 is multi-page; most routes render a
single content block with nothing to navigate *between*. Rather than
force chapter markers onto pages that don't have real sections, only
`home.php` (hero, symptom-strip, conditions teaser — 3 chapters) and
`cv.php` (7 real `cv-section` blocks: appointments, education, expertise,
memberships, publications, languages, additional experience — read
directly to confirm this before deciding) got `[data-chapter]` markers.
`cv.php` turned out to be the better fit of the two, with more real
structure than the homepage itself.

`thread.js` uses `[data-thread]` as its JS hook rather than reusing the
`.thread` styling class for both — matches web2's own established
convention (`nav.js` already does the same with `[data-nav]` etc.),
a deliberate improvement over blindly porting web3.1's class-does-both
pattern. `--thread-live` resolves to web2's own `--color-oxblood`, not a
dangling reference to web3.1's `--ox-700`, which doesn't exist here.

`.thread` stays `opacity: 0` by default rather than following the
"visible until JS proves itself" progressive-enhancement rule web3.1's
own `.reveal`/`js-reveal` system (and web2's near-identical
`.js-reveal-ready`/`[data-reveal]` system) established. This is a
deliberate, reasoned exception, not an oversight: the Thread's path
geometry and every node position is computed from `getTotalLength()`/
`getPointAtLength()`, which cannot run without JS — there is no
meaningful non-JS state for it to show, unlike real content blocks. It's
`aria-hidden="true"` decoration and section-nav, not information.

## 5. Wayfinder: deferred, not fixed

The user's explicit call, after seeing the concrete gap list (a real
one-line CSS fix for an invisible-focus-ring bug, an empty-dialog
fallback, a tooltip truncation affordance, a `<dialog>`-unsupported
fallback, versus the two genuinely large items — thin content coverage
across the anatomy data, and interaction that has never been checked in
a real browser). Unhooked cleanly: `wayfinder.css`/`wayfinder.js` removed
from `layout.php`'s site-wide asset list, the wayfinder partial call
removed from `home.php` — three reference removals, nothing else.
`WayfinderLayout.php`, `templates/partials/wayfinder.php`, `wayfinder.js`,
and `wayfinder.css` are untouched on disk (verified byte-identical to
the `pre-merge-baseline` tag), ready to be re-attached whenever it's
picked up as its own future phase. Its gap list is unchanged and still
applies then.

## 6. Axis colors: verified close enough to leave alone

web2's `SpecialtyAxis` (Cranial/Spine/Pain → navy-mid/teal/oxblood) is
genuinely data-model-driven — audited every consumer across
`content.css`/`nav.css` and found zero decorative/page-wide usage, only
small bounded surfaces (hotspot fills, tag/chip borders, teaser-card
rules). web3.1's "three accent families read as noisy" objection was
about a page-wide *decorative* palette; it doesn't transfer to a small
functional tagging system, so the user's call to keep three real colors
rather than collapsing to two.

The plan going in assumed this would mean importing new hex values
(Cranial → web3.1's Ocean ramp, Spine → web3.1's retired Verdigris).
A direct RGB-distance check before touching anything showed that was
unnecessary: `--color-navy-mid` (#2C4A73) sits ~10 units (of ~441
possible) from web3.1's `--ocean-600` (#2C5470) — essentially the same
blue — and `--color-teal` (#1F5C52) sits ~33 units from web3.1's retired
`--green-700`/Verdigris (#2E6F68) — same muted family. The two palettes
had already converged on the same "Cranial=navy-blue, Spine=muted-teal"
territory independently. No token values were changed; the finding is
recorded in `tokens.css`'s header comment instead of trading a
verified-close match for an unverified new one for no real benefit.
Pain/`--color-oxblood` was never in question — unchanged, still doing
double duty as both the Pain axis and the sitewide CTA/action color, a
precedent that already existed in web2 before this merge.

## 7. Fonts: EB Garamond + Commissioner, genuinely self-hosted

Downloaded directly from Google Fonts' css2 API using web3.1's own
requested weight/style spec, not assumed or hand-typed. Google's
response splits each family into per-script `@font-face` blocks
automatically; only the `latin` and `greek` blocks were kept (matching
the two-script convention the old Alegreya/Noto Sans files already used
here — `greek-ext`/`cyrillic`/`latin-ext`/`vietnamese` dropped as
unneeded for a Greek/English site). Both normal styles turned out to be
genuine variable fonts on inspection (Commissioner wght 100-900,
EB Garamond wght 400-800) — every discrete weight in the request
resolved to the identical file, so each gets one `@font-face` with a
weight range instead of one rule per weight. EB Garamond italic has no
variable axis (static 400 only), matching what was actually requested.

Old Alegreya/Noto Sans files were removed outright, not kept as a
fallback chain, after confirming `public/assets/cv/styles.css` — the CV
attachment pages' own stylesheet, the one other place fonts get declared
in this project — uses an entirely separate Google-Fonts-CDN-loaded pair
(Playfair Display + Source Sans Pro) with no dependency on them.

## 8. What stayed exactly as it was

Per explicit scope: `src/Support/Config.php`'s three TODO placeholders
(`SITE_URL`, `GA4_MEASUREMENT_ID`, `GOOGLE_SITE_VERIFICATION`),
`PracticeData.php`/`ClinicData.php`'s already-self-documented staleness
(Henry Dunant should read "former"; Bioclinic Athens/Amarousio missing),
the 8 nav-reachable pages with no content yet, and the inert contact
forms — none of it touched, none of it invented. Verified with a diff
against the `pre-merge-baseline` tag, not just by memory of not having
edited those files.

## 9. The palette swap this record initially got wrong

Phase 6 (§6 above) shipped with web2's own Navy/Navy-mid/Teal/Oxblood
values unchanged, on the reasoning that they were already close enough
to web3.1's equivalents by measured RGB distance not to need retuning.
After deployment, the user reported the live site looked visually
identical to pre-merge web2 — correctly: that Phase 5 finding was about
whether web2's *existing* hex values needed adjustment for internal
harmony (they didn't), not about whether the final visible result should
still look like web2. It shouldn't have — the entire point of this merge
is that web3.1's design system is the *visible* basis, and color is the
single biggest driver of what a page "looks like."

Fixed by replacing every value in `tokens.css`'s palette section with
web3.1's own real measured colors, keeping the same token names/roles
web2 already had (`--color-navy-deep`, `--color-oxblood`, etc. — so
every component file's `var()` reference needed zero changes):

| Token | Old (web2) | New (web3.1-sourced) |
|---|---|---|
| `--color-field-white` (canvas) | `#F3F5F6` cool white | `#F4F4F2` (`--n-50`, warm paper) |
| `--color-bone-ink` (text) | `#141A21` | `#202A36` (`--text-display`, measured ink) |
| `--color-navy-deep` (chrome) | `#12294D` | `#04101E` (`--ocean-950`) |
| `--color-navy-mid` (links/Cranial) | `#2C4A73` | `#17384F` (`--ocean-700`) |
| `--color-teal` (Spine) | `#1F5C52` | `#2E6F68` (`--green-700`, retired Verdigris) |
| `--color-oxblood` (CTA/Pain) | `#6B1F2A` | `#521522` (`--ox-700`, the mockup-measured value) |

`--color-bone-ink` was confirmed (grep) to serve only as text color on
every currently-live page — its one background use is in `wayfinder.css`,
deferred since §5 — so it was safe to use web3.1's lighter *text* ink
rather than needing a darker "ground" tone as well. Shadow bases and
`--thread-idle` (recomputed once already in §2 from the old ink RGB) were
recomputed a second time from the new one, same reasoning both times.
Contrast re-verified after the swap, not assumed: bone-ink on the new
canvas 13.2:1, white nav text on the new navy-deep 17.4:1, links 11.1:1,
white CTA text on the new oxblood 12.8:1 — all comfortably past WCAG AA.

`--color-brass` was left alone (no web3.1 equivalent exists or is
needed). The two "-wash" families and `--focus-ring`/`--thread-live`
(already aliased to `--color-oxblood`, not a literal) update
automatically from the base tokens — no separate edits needed there.

## 10. Verification method throughout

No headless browser is available in this environment (same constraint
`~/web2`'s own `CLAUDE.md` already documents). Every phase was verified
with: `php8.2 -l` on every touched PHP file, `node --check`/
`node --input-type=module --check` on every touched JS file, a local
`php -S` server with every one of the 126 real sitemap URLs (63
destinations × 2 locales) curled for HTTP 200 and checked for regressions
after every single phase, and `DataStore::integrityErrors()` as a data
safety net. What curl and lint cannot verify - the mega-panel's actual
visual layout, the Thread's scroll-driven draw and node ignition, font
glyph rendering, the retuned type scale and axis colors, actually looking
right - needs the real `https://nyx.erns.eu/mweb/web3p1/` Apache
deployment and a human in a browser. Each phase is its own tagged commit
(`phase-0-scaffold` through `phase-6-fonts`) specifically so that pass can
target and bisect a single phase if something needs it.
