--- baseline/PROGRESS.md
+++ candidate/PROGRESS.md
@@ -1,19 +1,33 @@
 # Progress
 
-Status: starter repository received
+Status: vertical slice implemented
 
 ## Completed
 
-- [ ] Responsive page composition
-- [ ] Filter and URL state
-- [ ] Service detail interaction
-- [ ] Accessible enquiry flow
-- [ ] Content/missing-media resilience
-- [ ] Reduced-motion treatment
-- [ ] Type check
-- [ ] Production build
+- [x] Responsive page composition (360 / 768 / 1440 CSS-px)
+- [x] Header with keyboard-usable mobile menu (Escape closes, focus returns)
+- [x] Hero + service explorer using supplied content and local SVG assets
+- [x] Filter and URL state (?discipline=all|electrical|controls|reliability; back/forward via popstate)
+- [x] Service detail interaction (native focusable buttons, aria-pressed, detail region)
+- [x] Accessible scope-review dialog (focus trap, Escape, focus restore, aria-modal)
+- [x] Form validation (required name/email/projectType; valid email; >=20 non-ws summary; native select)
+- [x] Exact success message on valid submit (no network request)
+- [x] Content/missing-media resilience (power-quality designed fallback)
+- [x] Long title preserved (switchboard-modernisation, no truncation)
+- [x] Reduced-motion treatment (motion useReducedMotion + CSS overrides)
+- [x] Evaluator hooks preserved
+- [x] Type check
+- [x] Production build
 
 ## Verification evidence
 
-Record commands and outcomes here. Keep this concise and factual.
-
+- `npm run check` (tsc -b --pretty false) -> exit 0, no diagnostics.
+- `npm run build` -> exit 0; output dist/index.html, dist/assets/index-*.css (14.30 kB), dist/assets/index-*.js (338.23 kB); 426 modules transformed in 172ms.
+- `npm run dev` -> Vite ready in 133ms, no startup errors.
+- data-testid hooks confirmed present across Header, Hero, Services, Sections, ScopeReviewDialog.
+
+## Notes
+
+- Design: warm off-white paper (#f1efe7), ink/steel tones, single citron accent (#d5ff43) used sparingly; schematic hero panel substitutes for absent hero photography.
+- Motion: entrance fades + layout transitions on filter/selection via `motion/react`; all JS animation gated on `useReducedMotion()`, CSS transitions neutralised under `prefers-reduced-motion: reduce`.
+- No new dependencies, fonts, remote assets, or backend added.

--- baseline/src/App.tsx
+++ candidate/src/App.tsx
@@ -1,42 +1,41 @@
+import { useCallback, useState } from 'react'
 import { content } from './content'
+import { useDisciplineParam } from './useDisciplineParam'
+import { Header } from './components/Header'
+import { Hero } from './components/Hero'
+import { Services } from './components/Services'
+import { Approach, Close, Footer } from './components/Sections'
+import { ScopeReviewDialog } from './components/ScopeReviewDialog'
 
 function App() {
+  const [dialogOpen, setDialogOpen] = useState(false)
+  const { discipline, setDiscipline } = useDisciplineParam()
+
+  const openScope = useCallback(() => setDialogOpen(true), [])
+  const closeScope = useCallback(() => setDialogOpen(false), [])
+
   return (
     <div className="site-shell">
-      <header className="site-header">
-        <a className="brand" href="#top" aria-label="Gridline Field Services home">
-          <strong>{content.brand}</strong>
-          <span>{content.descriptor}</span>
-        </a>
-        <nav aria-label="Primary navigation">
-          {content.nav.map((item) => <a key={item.href} href={item.href}>{item.label}</a>)}
-        </nav>
-        <button type="button" data-testid="scope-review-open">{content.hero.primaryAction}</button>
-      </header>
+      <Header onOpenScope={openScope} />
 
       <main id="top">
-        <section className="hero">
-          <p className="eyebrow">{content.hero.eyebrow}</p>
-          <h1>{content.hero.title}</h1>
-          <p>{content.hero.body}</p>
-          <a href="#services">{content.hero.secondaryAction}</a>
-        </section>
-
-        <section id="services" className="services">
-          <p className="eyebrow">{content.servicesIntro.eyebrow}</p>
-          <h2>{content.servicesIntro.title}</h2>
-          <p>{content.servicesIntro.body}</p>
-          <div className="service-grid">
-            {content.services.map((service) => (
-              <article key={service.id} data-testid={`service-card-${service.id}`}>
-                <p>{service.discipline}</p>
-                <h3>{service.title}</h3>
-                <p>{service.summary}</p>
-              </article>
-            ))}
-          </div>
-        </section>
+        <Hero onOpenScope={openScope} />
+        <Services discipline={discipline} onDisciplineChange={setDiscipline} />
+        <Approach />
+        <Close onOpenScope={openScope} />
       </main>
+
+      <Footer />
+
+      <ScopeReviewDialog
+        open={dialogOpen}
+        onClose={closeScope}
+        title={content.form.title}
+        intro={content.form.intro}
+        projectTypes={content.form.projectTypes}
+        submitLabel={content.form.submit}
+        success={content.form.success}
+      />
     </div>
   )
 }

--- baseline/src/components/Header.tsx
+++ candidate/src/components/Header.tsx
@@ -0,0 +1,88 @@
+import { useEffect, useRef, useState } from 'react'
+import { content } from '../content'
+
+export function Header({ onOpenScope }: { onOpenScope: () => void }) {
+  const [open, setOpen] = useState(false)
+  const toggleRef = useRef<HTMLButtonElement>(null)
+
+  useEffect(() => {
+    if (!open) return
+    const onKey = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') {
+        setOpen(false)
+        toggleRef.current?.focus()
+      }
+    }
+    document.addEventListener('keydown', onKey)
+    return () => document.removeEventListener('keydown', onKey)
+  }, [open])
+
+  return (
+    <header className="site-header">
+      <a className="brand" href="#top" aria-label="Gridline Field Services home">
+        <strong>{content.brand}</strong>
+        <span>{content.descriptor}</span>
+      </a>
+
+      <nav className="nav-desktop" aria-label="Primary navigation">
+        {content.nav.map((item) => (
+          <a key={item.href} href={item.href}>{item.label}</a>
+        ))}
+      </nav>
+
+      <div className="header-actions">
+        <button
+          type="button"
+          className="btn btn-primary"
+          data-testid="scope-review-open"
+          onClick={onOpenScope}
+        >
+          {content.hero.primaryAction}
+        </button>
+        <button
+          ref={toggleRef}
+          type="button"
+          className="mobile-menu-toggle"
+          aria-expanded={open}
+          aria-controls="mobile-menu"
+          aria-label={open ? 'Close menu' : 'Open menu'}
+          data-testid="mobile-menu-toggle"
+          onClick={() => setOpen((o) => !o)}
+        >
+          <span className="menu-bar" data-open={open} />
+          <span className="sr-only">{open ? 'Close menu' : 'Open menu'}</span>
+        </button>
+      </div>
+
+      {open && (
+        <div
+          id="mobile-menu"
+          className="mobile-menu"
+          data-testid="mobile-menu"
+          role="region"
+          aria-label="Mobile navigation"
+        >
+          <nav aria-label="Mobile primary navigation">
+            {content.nav.map((item) => (
+              <a
+                key={item.href}
+                href={item.href}
+                onClick={() => { setOpen(false); toggleRef.current?.focus() }}
+              >
+                {item.label}
+              </a>
+            ))}
+            <button
+              type="button"
+              className="btn btn-primary"
+              data-testid="scope-review-open"
+              onClick={() => { setOpen(false); onOpenScope() }}
+            >
+              {content.hero.primaryAction}
+            </button>
+          </nav>
+        </div>
+      )}
+    </header>
+  )
+}

--- baseline/src/components/Hero.tsx
+++ candidate/src/components/Hero.tsx
@@ -0,0 +1,61 @@
+import { motion } from 'motion/react'
+import { useReducedMotion } from 'motion/react'
+import { content } from '../content'
+
+export function Hero({ onOpenScope }: { onOpenScope: () => void }) {
+  const reduce = useReducedMotion()
+  const fade = reduce
+    ? { initial: false as const, animate: { opacity: 1 } as const }
+    : { initial: { opacity: 0, y: 12 } as const, animate: { opacity: 1, y: 0 } as const }
+
+  return (
+    <section className="hero" id="top" aria-labelledby="hero-title">
+      <div className="hero-grid">
+        <motion.div className="hero-copy" {...fade} transition={{ duration: 0.5, ease: 'easeOut' }}>
+          <p className="eyebrow">{content.hero.eyebrow}</p>
+          <h1 id="hero-title">{content.hero.title}</h1>
+          <p className="hero-body">{content.hero.body}</p>
+          <div className="hero-actions">
+            <button
+              type="button"
+              className="btn btn-primary"
+              data-testid="scope-review-open"
+              onClick={onOpenScope}
+            >
+              {content.hero.primaryAction}
+            </button>
+            <a className="btn btn-ghost" href="#services">{content.hero.secondaryAction}</a>
+          </div>
+          <p className="hero-availability" aria-label="Availability">
+            <span className="availability-dot" aria-hidden="true" />
+            {content.hero.availability}
+          </p>
+        </motion.div>
+
+        <motion.div
+          className="hero-media"
+          initial={reduce ? false : { opacity: 0, scale: 0.99 }}
+          animate={{ opacity: 1, scale: 1 }}
+          transition={{ duration: 0.6, ease: 'easeOut', delay: reduce ? 0 : 0.08 }}
+          aria-hidden="true"
+        >
+          <div className="hero-panel">
+            <div className="panel-header">
+              <span className="panel-tag">FIELD NOTE / VIC</span>
+              <span className="panel-coord">37.81°S · 144.96°E</span>
+            </div>
+            <div className="panel-rules" aria-hidden="true">
+              <span /><span /><span /><span />
+            </div>
+            <div className="panel-body">
+              <div className="panel-row"><span>SECTOR</span><strong>Industrial electrical + automation</strong></div>
+              <div className="panel-row"><span>MODE</span><strong>Planned · Response</strong></div>
+              <div className="panel-row"><span>SCOPE</span><strong>Define → Deliver → Handover</strong></div>
+              <div className="panel-bar" aria-hidden="true"><i style={{ width: '68%' }} /></div>
+            </div>
+          </div>
+        </motion.div>
+      </div>
+    </section>
+  )
+}

--- baseline/src/components/ScopeReviewDialog.tsx
+++ candidate/src/components/ScopeReviewDialog.tsx
@@ -0,0 +1,255 @@
+import { useEffect, useRef, useState } from 'react'
+
+type Field = 'name' | 'email' | 'projectType' | 'summary'
+
+type Errors = Partial<Record<Field, string>>
+type Values = Record<Field, string>
+
+const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+
+function nonWhitespaceLength(s: string) {
+  return s.replace(/\s/g, '').length
+}
+
+export function ScopeReviewDialog({
+  open,
+  onClose,
+  title,
+  intro,
+  projectTypes,
+  submitLabel,
+  success,
+}: {
+  open: boolean
+  onClose: () => void
+  title: string
+  intro: string
+  projectTypes: readonly string[]
+  submitLabel: string
+  success: string
+}) {
+  const dialogRef = useRef<HTMLDivElement>(null)
+  const openerRef = useRef<HTMLElement | null>(null)
+  const firstFieldRef = useRef<HTMLInputElement | null>(null)
+  const closeBtnRef = useRef<HTMLButtonElement | null>(null)
+  const [values, setValues] = useState<Values>({ name: '', email: '', projectType: '', summary: '' })
+  const [errors, setErrors] = useState<Errors>({})
+  const [submitted, setSubmitted] = useState(false)
+
+  useEffect(() => {
+    if (!open) return
+    openerRef.current = document.activeElement as HTMLElement | null
+    const previouslyFocused = openerRef.current
+    const t = window.setTimeout(() => {
+      if (closeBtnRef.current) closeBtnRef.current.focus()
+      else if (firstFieldRef.current) firstFieldRef.current.focus()
+    }, 0)
+    const prevOverflow = document.body.style.overflow
+    document.body.style.overflow = 'hidden'
+
+    const onKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') {
+        e.preventDefault()
+        onClose()
+        return
+      }
+      if (e.key !== 'Tab') return
+      const node = dialogRef.current
+      if (!node) return
+      const focusables = node.querySelectorAll<HTMLElement>(
+        'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
+      )
+      if (focusables.length === 0) return
+      const list = Array.from(focusables)
+      const first = list[0]
+      const last = list[list.length - 1]
+      const active = document.activeElement as HTMLElement | null
+      if (e.shiftKey) {
+        if (active === first || !node.contains(active)) {
+          e.preventDefault()
+          last.focus()
+        }
+      } else {
+        if (active === last) {
+          e.preventDefault()
+          first.focus()
+        }
+      }
+    }
+    document.addEventListener('keydown', onKeyDown)
+
+    return () => {
+      window.clearTimeout(t)
+      document.removeEventListener('keydown', onKeyDown)
+      document.body.style.overflow = prevOverflow
+      if (previouslyFocused && typeof previouslyFocused.focus === 'function') {
+        previouslyFocused.focus()
+      }
+    }
+  }, [open, onClose])
+
+  if (!open) return null
+
+  const update = (field: Field, value: string) => {
+    setValues((v) => ({ ...v, [field]: value }))
+    if (errors[field]) setErrors((e) => ({ ...e, [field]: undefined }))
+  }
+
+  const validate = (): Errors => {
+    const next: Errors = {}
+    if (!values.name.trim()) next.name = 'Enter your name.'
+    if (!values.email.trim()) next.email = 'Enter a work email.'
+    else if (!EMAIL_RE.test(values.email.trim())) next.email = 'Enter a valid email address.'
+    if (!values.projectType) next.projectType = 'Choose a project type.'
+    if (nonWhitespaceLength(values.summary) < 20) next.summary = 'Summary needs at least 20 characters of content.'
+    return next
+  }
+
+  const onSubmit = (e: React.FormEvent) => {
+    e.preventDefault()
+    const next = validate()
+    setErrors(next)
+    if (Object.keys(next).length > 0) {
+      const firstErrorField = (['name', 'email', 'projectType', 'summary'] as Field[]).find((f) => next[f])
+      if (firstErrorField) {
+        const el = dialogRef.current?.querySelector<HTMLElement>(`[name="${firstErrorField}"]`)
+        el?.focus()
+      }
+      return
+    }
+    setSubmitted(true)
+  }
+
+  const resetForm = () => {
+    setSubmitted(false)
+    setValues({ name: '', email: '', projectType: '', summary: '' })
+    setErrors({})
+  }
+
+  const describedBy = (field: Field) => (errors[field] ? `error-${field}` : undefined)
+
+  return (
+    <div
+      className="dialog-scrim"
+      role="presentation"
+      onClick={(e) => {
+        if (e.target === e.currentTarget) onClose()
+      }}
+    >
+      <div
+        ref={dialogRef}
+        className="dialog"
+        role="dialog"
+        aria-modal="true"
+        aria-labelledby="scope-review-title"
+        data-testid="scope-review-dialog"
+      >
+        <div className="dialog-head">
+          <div>
+            <p className="eyebrow">Scope review</p>
+            <h2 id="scope-review-title">{title}</h2>
+            <p className="dialog-intro">{intro}</p>
+          </div>
+          <button
+            ref={closeBtnRef}
+            type="button"
+            className="dialog-close"
+            aria-label="Close scope review dialog"
+            onClick={onClose}
+          >
+            ×
+          </button>
+        </div>
+
+        {submitted ? (
+          <div className="dialog-success" data-testid="scope-review-success" role="status">
+            <p>{success}</p>
+            <button type="button" className="btn btn-ghost" onClick={() => { resetForm(); onClose() }}>
+              Close
+            </button>
+          </div>
+        ) : (
+          <form className="scope-form" data-testid="scope-review-form" noValidate onSubmit={onSubmit}>
+            <div className="field">
+              <label htmlFor="f-name">Name</label>
+              <input
+                id="f-name"
+                name="name"
+                ref={firstFieldRef}
+                type="text"
+                value={values.name}
+                autoComplete="name"
+                aria-invalid={!!errors.name}
+                aria-describedby={describedBy('name')}
+                onChange={(e) => update('name', e.target.value)}
+              />
+              {errors.name && (
+                <p className="field-error" id="error-name" data-testid="error-name" role="alert">{errors.name}</p>
+              )}
+            </div>
+
+            <div className="field">
+              <label htmlFor="f-email">Work email</label>
+              <input
+                id="f-email"
+                name="email"
+                type="email"
+                value={values.email}
+                autoComplete="email"
+                aria-invalid={!!errors.email}
+                aria-describedby={describedBy('email')}
+                onChange={(e) => update('email', e.target.value)}
+              />
+              {errors.email && (
+                <p className="field-error" id="error-email" data-testid="error-email" role="alert">{errors.email}</p>
+              )}
+            </div>
+
+            <div className="field">
+              <label htmlFor="f-projectType">Project type</label>
+              <select
+                id="f-projectType"
+                name="projectType"
+                value={values.projectType}
+                aria-invalid={!!errors.projectType}
+                aria-describedby={describedBy('projectType')}
+                onChange={(e) => update('projectType', e.target.value)}
+              >
+                <option value="" disabled>Select…</option>
+                {projectTypes.map((t) => (
+                  <option key={t} value={t}>{t}</option>
+                ))}
+              </select>
+              {errors.projectType && (
+                <p className="field-error" id="error-projectType" data-testid="error-projectType" role="alert">{errors.projectType}</p>
+              )}
+            </div>
+
+            <div className="field">
+              <label htmlFor="f-summary">Project summary</label>
+              <textarea
+                id="f-summary"
+                name="summary"
+                rows={5}
+                value={values.summary}
+                aria-invalid={!!errors.summary}
+                aria-describedby={describedBy('summary')}
+                onChange={(e) => update('summary', e.target.value)}
+              />
+              {errors.summary ? (
+                <p className="field-error" id="error-summary" data-testid="error-summary" role="alert">{errors.summary}</p>
+              ) : (
+                <p className="field-hint">At least 20 non-whitespace characters.</p>
+              )}
+            </div>
+
+            <div className="form-actions">
+              <button type="submit" className="btn btn-primary">{submitLabel}</button>
+              <button type="button" className="btn btn-ghost" onClick={onClose}>Cancel</button>
+            </div>
+          </form>
+        )}
+      </div>
+    </div>
+  )
+}

--- baseline/src/components/Sections.tsx
+++ candidate/src/components/Sections.tsx
@@ -0,0 +1,54 @@
+import { content } from '../content'
+
+export function Approach() {
+  return (
+    <section id="approach" className="approach" aria-labelledby="approach-title">
+      <div className="section-intro">
+        <p className="eyebrow">{content.approach.eyebrow}</p>
+        <h2 id="approach-title">{content.approach.title}</h2>
+      </div>
+      <ol className="principles" role="list">
+        {content.approach.principles.map((p) => (
+          <li key={p.number} className="principle">
+            <span className="principle-number">{p.number}</span>
+            <h3 className="principle-title">{p.title}</h3>
+            <p className="principle-body">{p.body}</p>
+          </li>
+        ))}
+      </ol>
+    </section>
+  )
+}
+
+export function Close({ onOpenScope }: { onOpenScope: () => void }) {
+  return (
+    <section className="close" aria-labelledby="close-title">
+      <p className="eyebrow">{content.close.eyebrow}</p>
+      <h2 id="close-title">{content.close.title}</h2>
+      <p className="close-body">{content.close.body}</p>
+      <button
+        type="button"
+        className="btn btn-primary btn-lg"
+        data-testid="scope-review-open"
+        onClick={onOpenScope}
+      >
+        {content.hero.primaryAction}
+      </button>
+    </section>
+  )
+}
+
+export function Footer() {
+  return (
+    <footer className="site-footer">
+      <div className="footer-brand">
+        <strong>{content.brand}</strong>
+        <span>{content.footer.line}</span>
+      </div>
+      <div className="footer-meta">
+        <p>{content.footer.region}</p>
+        <p>{content.footer.note}</p>
+      </div>
+    </footer>
+  )
+}

--- baseline/src/components/Services.tsx
+++ candidate/src/components/Services.tsx
@@ -0,0 +1,174 @@
+import { useMemo, useState } from 'react'
+import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
+import { content } from '../content'
+import type { Discipline, Service } from '../content'
+
+function ServiceCard({
+  service,
+  selected,
+  onSelect,
+}: {
+  service: Service
+  selected: boolean
+  onSelect: (id: string) => void
+}) {
+  return (
+    <article
+      className="service-card"
+      data-testid={`service-card-${service.id}`}
+      data-selected={selected}
+    >
+      <div className="service-card-media">
+        {service.image ? (
+          <img src={service.image} alt={service.imageAlt} loading="lazy" />
+        ) : (
+          <div className="media-fallback" data-testid="media-fallback" role="img" aria-label={service.imageAlt}>
+            <span className="media-fallback-tag">NO FIELD IMAGE</span>
+            <span className="media-fallback-id">{service.id}</span>
+            <span className="media-fallback-note">Evidence captured in report, not in photograph.</span>
+          </div>
+        )}
+      </div>
+      <p className="service-discipline">{service.discipline}</p>
+      <h3 className="service-title">{service.title}</h3>
+      <p className="service-summary">{service.summary}</p>
+      <button
+        type="button"
+        className="btn btn-ghost service-select"
+        aria-pressed={selected}
+        onClick={() => onSelect(service.id)}
+      >
+        {selected ? 'Showing detail' : 'Inspect service'}
+      </button>
+    </article>
+  )
+}
+
+export function Services({
+  discipline,
+  onDisciplineChange,
+}: {
+  discipline: Discipline
+  onDisciplineChange: (d: Discipline) => void
+}) {
+  const [selectedId, setSelectedId] = useState<string | null>(null)
+  const reduce = useReducedMotion()
+
+  const visible = useMemo(
+    () => content.services.filter((s) => discipline === 'all' || s.discipline === discipline),
+    [discipline],
+  )
+
+  const selected = content.services.find((s) => s.id === selectedId) ?? null
+
+  const onSelect = (id: string) => setSelectedId((cur) => (cur === id ? null : id))
+
+  return (
+    <section id="services" className="services" aria-labelledby="services-title">
+      <div className="section-intro">
+        <p className="eyebrow">{content.servicesIntro.eyebrow}</p>
+        <h2 id="services-title">{content.servicesIntro.title}</h2>
+        <p className="section-body">{content.servicesIntro.body}</p>
+      </div>
+
+      <div className="filters" role="group" aria-label="Filter services by discipline">
+        {content.filters.map((f) => {
+          const active = discipline === f.value
+          return (
+            <button
+              key={f.value}
+              type="button"
+              className="filter-btn"
+              data-testid={`filter-${f.value}`}
+              aria-pressed={active}
+              data-active={active}
+              onClick={() => onDisciplineChange(f.value)}
+            >
+              {f.label}
+            </button>
+          )
+        })}
+      </div>
+
+      <p className="service-count" data-testid="service-count">
+        <span className="count-number">{visible.length}</span>
+        <span>{visible.length === 1 ? 'service' : 'services'} available</span>
+        {discipline !== 'all' && <span className="count-filter"> · {discipline}</span>}
+      </p>
+
+      <div className="service-layout">
+        <ul className="service-grid" role="list">
+          <AnimatePresence mode="popLayout" initial={false}>
+            {visible.map((service) => (
+              <motion.li
+                key={service.id}
+                layout={!reduce}
+                initial={reduce ? false : { opacity: 0, y: 8 }}
+                animate={{ opacity: 1, y: 0 }}
+                exit={reduce ? undefined : { opacity: 0, y: -8 }}
+                transition={{ duration: 0.25, ease: 'easeOut' }}
+                style={{ listStyle: 'none' }}
+              >
+                <ServiceCard
+                  service={service}
+                  selected={selected?.id === service.id}
+                  onSelect={onSelect}
+                />
+              </motion.li>
+            ))}
+          </AnimatePresence>
+        </ul>
+
+        <div
+          className="service-detail"
+          data-testid="service-detail"
+          aria-live="polite"
+          aria-atomic="true"
+        >
+          <AnimatePresence mode="wait">
+            {selected ? (
+              <motion.div
+                key={selected.id}
+                initial={reduce ? false : { opacity: 0, y: 8 }}
+                animate={{ opacity: 1, y: 0 }}
+                exit={reduce ? undefined : { opacity: 0, y: -8 }}
+                transition={{ duration: 0.25, ease: 'easeOut' }}
+              >
+                <p className="eyebrow">Service detail</p>
+                <h3 className="detail-title">{selected.title}</h3>
+                <p className="detail-discipline">{selected.discipline}</p>
+                <p className="detail-description">{selected.description}</p>
+                <div className="detail-deliverables">
+                  <h4>Deliverables</h4>
+                  <ul>
+                    {selected.deliverables.map((d) => (
+                      <li key={d}>{d}</li>
+                    ))}
+                  </ul>
+                </div>
+                <p className="detail-response">
+                  <span className="response-label">Response note</span>
+                  <span>{selected.responseNote}</span>
+                </p>
+              </motion.div>
+            ) : (
+              <motion.div
+                key="empty"
+                initial={reduce ? false : { opacity: 0 }}
+                animate={{ opacity: 1 }}
+                exit={reduce ? undefined : { opacity: 0 }}
+                transition={{ duration: 0.2 }}
+                className="detail-empty"
+              >
+                <p className="eyebrow">Service detail</p>
+                <p className="detail-empty-body">
+                  Select a service to read its description, deliverables and response note.
+                </p>
+              </motion.div>
+            )}
+          </AnimatePresence>
+        </div>
+      </div>
+    </section>
+  )
+}

--- baseline/src/styles.css
+++ candidate/src/styles.css
@@ -1,37 +1,376 @@
 :root {
-  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-  color: #18211f;
-  background: #f1efe7;
+  --paper: #f1efe7;
+  --paper-2: #ebe8de;
+  --ink: #18211f;
+  --ink-2: #2a3531;
+  --steel: #4a5550;
+  --rule: #c6c9c0;
+  --rule-strong: #a9ada3;
+  --accent: #d5ff43;
+  --accent-ink: #1c2a12;
+  --danger: #8a2b2b;
+  --radius: 4px;
+  --radius-lg: 8px;
+  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+  color: var(--ink);
+  background: var(--paper);
   font-synthesis: none;
   text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
 }
 
 * { box-sizing: border-box; }
 html { scroll-behavior: smooth; }
-body { margin: 0; min-width: 320px; }
-button, input, select, textarea { font: inherit; }
+body { margin: 0; min-width: 320px; background: var(--paper); color: var(--ink); }
+button, input, select, textarea { font: inherit; color: inherit; }
 a { color: inherit; }
-.site-shell { min-height: 100vh; }
-.site-header { display: flex; align-items: center; justify-content: space-between; gap: 2rem; padding: 1rem 4vw; border-bottom: 1px solid #b7bbb4; }
-.brand { display: grid; text-decoration: none; }
-.brand span, .eyebrow { font-size: .75rem; letter-spacing: .08em; text-transform: uppercase; }
-nav { display: flex; gap: 1.5rem; }
-.hero, .services { padding: 5rem 6vw; }
-.hero { min-height: 55vh; display: grid; align-content: center; max-width: 74rem; }
-h1 { max-width: 10ch; margin: .25em 0; font-size: clamp(3rem, 9vw, 7rem); line-height: .9; letter-spacing: -.06em; }
-.hero > p:not(.eyebrow) { max-width: 42rem; font-size: 1.15rem; line-height: 1.6; }
-.services { border-top: 1px solid #b7bbb4; }
-.services > h2 { max-width: 18ch; font-size: clamp(2rem, 5vw, 4.5rem); line-height: 1; }
-.service-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; margin-top: 3rem; background: #aeb4ae; border: 1px solid #aeb4ae; }
-.service-grid article { min-width: 0; padding: 1.5rem; background: #f1efe7; }
-
-@media (max-width: 760px) {
-  .site-header nav { display: none; }
-  .site-header > button { display: none; }
-  .service-grid { grid-template-columns: 1fr; }
+h1, h2, h3, h4, p, ul, ol { margin: 0; }
+ul, ol { padding: 0; }
+
+.sr-only {
+  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
+  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
+}
+
+.eyebrow {
+  font-size: .72rem; letter-spacing: .18em; text-transform: uppercase;
+  color: var(--steel); font-weight: 600;
+}
+
+/* ---------- Buttons ---------- */
+.btn {
+  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
+  padding: .7rem 1.15rem; border: 1px solid var(--ink); border-radius: var(--radius);
+  font-size: .95rem; font-weight: 600; text-decoration: none; cursor: pointer;
+  background: transparent; color: var(--ink); transition: background-color .15s ease, color .15s ease, border-color .15s ease;
+  min-height: 44px;
+}
+.btn:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
+.btn-primary { background: var(--ink); color: var(--paper); border-color: var(--ink); }
+.btn-primary:hover { background: var(--accent-ink); }
+.btn-ghost { background: transparent; border-color: var(--ink); color: var(--ink); }
+.btn-ghost:hover { background: var(--ink); color: var(--paper); }
+.btn-lg { padding: .95rem 1.6rem; font-size: 1.05rem; }
+
+/* ---------- Header ---------- */
+.site-header {
+  position: sticky; top: 0; z-index: 30;
+  display: flex; align-items: center; gap: 1.25rem;
+  padding: .85rem 5vw;
+  background: rgba(241, 239, 231, .9);
+  backdrop-filter: saturate(120%);
+  border-bottom: 1px solid var(--rule);
+}
+.brand { display: grid; text-decoration: none; line-height: 1; }
+.brand strong { font-size: 1.05rem; letter-spacing: .14em; }
+.brand span { font-size: .68rem; letter-spacing: .12em; text-transform: uppercase; color: var(--steel); }
+.nav-desktop { display: none; gap: 1.5rem; margin-left: auto; }
+.nav-desktop a {
+  text-decoration: none; font-size: .92rem; font-weight: 500; padding: .4rem 0;
+  border-bottom: 2px solid transparent; transition: border-color .15s ease;
+}
+.nav-desktop a:hover, .nav-desktop a:focus-visible { border-bottom-color: var(--ink); }
+.header-actions { margin-left: auto; display: flex; align-items: center; gap: .65rem; }
+.header-actions .btn-primary { display: none; }
+
+.mobile-menu-toggle {
+  width: 44px; height: 44px; display: inline-grid; place-items: center;
+  background: transparent; border: 1px solid var(--rule-strong); border-radius: var(--radius);
+  cursor: pointer; position: relative;
+}
+.mobile-menu-toggle:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
+.menu-bar, .menu-bar::before, .menu-bar::after {
+  content: ""; display: block; width: 18px; height: 2px; background: var(--ink);
+  transition: transform .2s ease, opacity .15s ease;
+}
+.menu-bar::before { position: absolute; transform: translateY(-6px); }
+.menu-bar::after { position: absolute; transform: translateY(6px); }
+.menu-bar[data-open="true"] { background: transparent; }
+.menu-bar[data-open="true"]::before { transform: rotate(45deg); }
+.menu-bar[data-open="true"]::after { transform: rotate(-45deg); }
+
+.mobile-menu {
+  position: absolute; top: 100%; left: 0; right: 0;
+  background: var(--paper); border-bottom: 1px solid var(--rule);
+  padding: 1rem 5vw 1.5rem;
+  box-shadow: 0 18px 30px -22px rgba(24,33,31,.35);
+}
+.mobile-menu nav { display: grid; gap: .25rem; }
+.mobile-menu nav a {
+  text-decoration: none; font-weight: 600; font-size: 1.05rem;
+  padding: .8rem 0; border-bottom: 1px solid var(--rule);
+}
+.mobile-menu nav .btn { margin-top: .9rem; }
+
+/* ---------- Hero ---------- */
+.hero { padding: 3.5rem 5vw 3rem; border-bottom: 1px solid var(--rule); }
+.hero-grid { display: grid; gap: 2.5rem; align-items: center; }
+.hero-copy { max-width: 42rem; }
+.hero h1 {
+  margin: .6rem 0 .9rem;
+  font-size: clamp(2.6rem, 11vw, 5.2rem);
+  line-height: .92; letter-spacing: -.04em; font-weight: 800;
+  max-width: 12ch;
+}
+.hero-body { font-size: 1.1rem; line-height: 1.6; color: var(--ink-2); max-width: 38rem; }
+.hero-actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 1.5rem; }
+.hero-availability {
+  display: inline-flex; align-items: center; gap: .55rem;
+  margin-top: 1.4rem; font-size: .82rem; color: var(--steel);
+  letter-spacing: .04em;
+}
+.availability-dot {
+  width: 8px; height: 8px; border-radius: 50%; background: var(--accent);
+  box-shadow: 0 0 0 3px rgba(213,255,67,.35);
+}
+
+.hero-media { justify-self: stretch; }
+.hero-panel {
+  background: var(--ink); color: var(--paper);
+  border: 1px solid var(--ink); border-radius: var(--radius-lg);
+  overflow: hidden; position: relative;
+}
+.panel-header {
+  display: flex; justify-content: space-between; align-items: center;
+  padding: .7rem 1rem; border-bottom: 1px solid rgba(241,239,231,.14);
+  font-size: .68rem; letter-spacing: .14em; text-transform: uppercase;
+  color: rgba(241,239,231,.7);
+}
+.panel-coord { font-variant-numeric: tabular-nums; }
+.panel-rules {
+  display: grid; grid-template-columns: repeat(4, 1fr); gap: 0;
+  border-bottom: 1px solid rgba(241,239,231,.14);
+}
+.panel-rules span { height: 14px; border-right: 1px solid rgba(241,239,231,.12); }
+.panel-rules span:last-child { border-right: 0; }
+.panel-body { padding: 1.2rem 1rem 1.4rem; display: grid; gap: .85rem; }
+.panel-row {
+  display: grid; grid-template-columns: 5.5rem 1fr; gap: .5rem; align-items: baseline;
+  font-size: .82rem;
+}
+.panel-row span { color: rgba(241,239,231,.55); letter-spacing: .12em; text-transform: uppercase; font-size: .68rem; }
+.panel-row strong { font-weight: 600; font-size: .95rem; }
+.panel-bar { height: 6px; background: rgba(241,239,231,.12); border-radius: 2px; overflow: hidden; margin-top: .4rem; }
+.panel-bar i { display: block; height: 100%; background: var(--accent); }
+
+/* ---------- Section intro ---------- */
+.section-intro { max-width: 46rem; margin-bottom: 2rem; }
+.section-intro h2 {
+  margin: .5rem 0 .8rem;
+  font-size: clamp(1.8rem, 5vw, 3.2rem); line-height: 1.02; letter-spacing: -.025em; font-weight: 800;
+}
+.section-body { font-size: 1.05rem; line-height: 1.6; color: var(--ink-2); }
+
+/* ---------- Services ---------- */
+.services { padding: 4rem 5vw; border-bottom: 1px solid var(--rule); }
+.filters {
+  display: flex; flex-wrap: wrap; gap: .5rem; padding: .35rem;
+  background: var(--paper-2); border: 1px solid var(--rule); border-radius: var(--radius);
+  width: fit-content; max-width: 100%;
+}
+.filter-btn {
+  padding: .5rem 1rem; border: 1px solid transparent; border-radius: 3px;
+  background: transparent; color: var(--ink-2); cursor: pointer; font-weight: 600;
+  font-size: .9rem; min-height: 36px; transition: background-color .15s ease, color .15s ease;
+}
+.filter-btn[data-active="true"] { background: var(--ink); color: var(--paper); }
+.filter-btn:not([data-active="true"]):hover { background: var(--paper); }
+.filter-btn:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
+
+.service-count {
+  margin-top: 1.1rem; display: flex; align-items: baseline; gap: .45rem;
+  font-size: .9rem; color: var(--steel); letter-spacing: .02em;
+}
+.count-number { font-size: 1.4rem; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
+.count-filter { text-transform: uppercase; letter-spacing: .12em; font-size: .72rem; }
+
+.service-layout {
+  margin-top: 1.8rem; display: grid; gap: 1.5rem;
+  grid-template-columns: 1fr;
+}
+.service-grid { display: grid; grid-template-columns: 1fr; gap: 1px; margin: 0; padding: 0; }
+.service-card {
+  min-width: 0; background: var(--paper); border: 1px solid var(--rule);
+  display: flex; flex-direction: column; gap: .55rem; padding: 1.1rem;
+  transition: border-color .15s ease, box-shadow .15s ease;
+}
+.service-card[data-selected="true"] { border-color: var(--ink); box-shadow: 0 0 0 2px var(--accent) inset; }
+.service-card-media {
+  aspect-ratio: 16 / 9; background: var(--paper-2); border: 1px solid var(--rule);
+  overflow: hidden; margin-bottom: .35rem;
+}
+.service-card-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
+.media-fallback {
+  width: 100%; height: 100%; position: relative;
+  display: grid; grid-template-rows: auto 1fr auto; gap: .5rem; padding: .8rem;
+  background:
+    repeating-linear-gradient(45deg, rgba(74,85,80,.08) 0 8px, transparent 8px 16px),
+    var(--paper-2);
+}
+.media-fallback-tag {
+  font-size: .62rem; letter-spacing: .16em; color: var(--steel); font-weight: 700;
+}
+.media-fallback-id {
+  align-self: center; text-align: center; font-weight: 800; letter-spacing: -.02em;
+  font-size: clamp(1.1rem, 4vw, 1.5rem); color: var(--ink-2);
+  border-top: 1px dashed var(--rule-strong); border-bottom: 1px dashed var(--rule-strong);
+  padding: .4rem 0;
+}
+.media-fallback-note { font-size: .72rem; color: var(--steel); line-height: 1.4; }
+
+.service-discipline {
+  font-size: .68rem; letter-spacing: .16em; text-transform: uppercase; color: var(--steel); font-weight: 700;
+}
+.service-title {
+  font-size: 1.1rem; line-height: 1.2; font-weight: 700; letter-spacing: -.01em;
+  word-break: break-word; hyphens: auto;
+}
+.service-summary { font-size: .95rem; line-height: 1.55; color: var(--ink-2); }
+.service-select { align-self: flex-start; margin-top: auto; }
+
+.service-detail {
+  background: var(--ink); color: var(--paper);
+  border: 1px solid var(--ink); border-radius: var(--radius-lg);
+  padding: 1.6rem; min-height: 12rem;
+}
+.service-detail .eyebrow { color: var(--accent); }
+.detail-title { font-size: 1.5rem; line-height: 1.15; font-weight: 800; letter-spacing: -.02em; margin: .4rem 0 .35rem; word-break: break-word; }
+.detail-discipline { font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; color: rgba(241,239,231,.6); }
+.detail-description { margin-top: .9rem; font-size: 1rem; line-height: 1.6; color: rgba(241,239,231,.92); }
+.detail-deliverables { margin-top: 1.2rem; }
+.detail-deliverables h4 { font-size: .72rem; letter-spacing: .16em; text-transform: uppercase; color: rgba(241,239,231,.6); margin-bottom: .5rem; }
+.detail-deliverables ul { list-style: none; display: grid; gap: .4rem; }
+.detail-deliverables li {
+  padding-left: 1.2rem; position: relative; font-size: .95rem; line-height: 1.5;
+  color: rgba(241,239,231,.92);
+}
+.detail-deliverables li::before {
+  content: ""; position: absolute; left: 0; top: .6rem; width: 8px; height: 2px; background: var(--accent);
+}
+.detail-response {
+  margin-top: 1.3rem; padding: .9rem 1rem; border-left: 3px solid var(--accent);
+  background: rgba(213,255,67,.08); border-radius: 2px;
+  display: grid; gap: .25rem;
+}
+.response-label { font-size: .68rem; letter-spacing: .16em; text-transform: uppercase; color: var(--accent); font-weight: 700; }
+.detail-response span:last-child { font-size: .95rem; line-height: 1.5; color: var(--paper); }
+.detail-empty .detail-empty-body { margin-top: .6rem; color: rgba(241,239,231,.6); font-size: .95rem; line-height: 1.6; }
+
+/* ---------- Approach ---------- */
+.approach { padding: 4rem 5vw; border-bottom: 1px solid var(--rule); background: var(--paper-2); }
+.principles { list-style: none; display: grid; gap: 1px; margin-top: 2rem; background: var(--rule); border: 1px solid var(--rule); }
+.principle { background: var(--paper-2); padding: 1.6rem; }
+.principle-number {
+  display: inline-block; font-size: .8rem; font-weight: 700; letter-spacing: .14em;
+  color: var(--ink); background: var(--accent); padding: .15rem .5rem; border-radius: 2px;
+}
+.principle-title { margin-top: .8rem; font-size: 1.25rem; line-height: 1.2; font-weight: 700; letter-spacing: -.01em; }
+.principle-body { margin-top: .5rem; font-size: .98rem; line-height: 1.6; color: var(--ink-2); max-width: 36rem; }
+
+/* ---------- Close ---------- */
+.close {
+  padding: 4rem 5vw; background: var(--ink); color: var(--paper);
+  border-bottom: 1px solid var(--ink);
+}
+.close .eyebrow { color: var(--accent); }
+.close h2 {
+  margin: .6rem 0 .8rem; font-size: clamp(1.8rem, 5vw, 3rem); line-height: 1.05; letter-spacing: -.025em; font-weight: 800; max-width: 18ch;
+}
+.close-body { font-size: 1.05rem; line-height: 1.6; color: rgba(241,239,231,.85); max-width: 42rem; }
+.close .btn-primary { margin-top: 1.6rem; background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
+.close .btn-primary:hover { background: var(--paper); border-color: var(--paper); }
+
+/* ---------- Footer ---------- */
+.site-footer {
+  padding: 2rem 5vw; display: flex; flex-wrap: wrap; gap: 1rem 2rem;
+  justify-content: space-between; align-items: flex-end;
+  background: var(--paper); border-top: 1px solid var(--rule);
+}
+.footer-brand { display: grid; line-height: 1.3; }
+.footer-brand strong { letter-spacing: .14em; }
+.footer-brand span { font-size: .82rem; color: var(--steel); }
+.footer-meta { text-align: right; font-size: .8rem; color: var(--steel); line-height: 1.5; }
+
+/* ---------- Dialog ---------- */
+.dialog-scrim {
+  position: fixed; inset: 0; z-index: 60;
+  background: rgba(24,33,31,.55);
+  display: grid; place-items: center; padding: 1rem;
+  overflow-y: auto;
+}
+.dialog {
+  background: var(--paper); color: var(--ink);
+  border: 1px solid var(--ink); border-radius: var(--radius-lg);
+  width: min(560px, 100%); max-height: calc(100vh - 2rem); overflow-y: auto;
+  box-shadow: 0 30px 60px -20px rgba(24,33,31,.5);
+}
+.dialog-head {
+  display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem;
+  padding: 1.3rem 1.4rem 1rem; border-bottom: 1px solid var(--rule);
+}
+.dialog-head h2 { font-size: 1.4rem; line-height: 1.1; letter-spacing: -.02em; font-weight: 800; margin-top: .3rem; }
+.dialog-intro { margin-top: .5rem; font-size: .92rem; color: var(--steel); line-height: 1.5; }
+.dialog-close {
+  width: 36px; height: 36px; flex: none; border: 1px solid var(--rule-strong);
+  background: transparent; border-radius: var(--radius); cursor: pointer; font-size: 1.3rem; line-height: 1;
+}
+.dialog-close:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
+
+.scope-form { padding: 1.2rem 1.4rem 1.4rem; display: grid; gap: 1rem; }
+.field { display: grid; gap: .4rem; }
+.field label { font-size: .82rem; font-weight: 600; letter-spacing: .02em; }
+.field input, .field select, .field textarea {
+  width: 100%; padding: .65rem .7rem; background: var(--paper);
+  border: 1px solid var(--rule-strong); border-radius: var(--radius);
+  font-size: 1rem; color: var(--ink); transition: border-color .15s ease, box-shadow .15s ease;
+}
+.field input:focus, .field select:focus, .field textarea:focus {
+  outline: none; border-color: var(--ink); box-shadow: 0 0 0 3px rgba(213,255,67,.4);
+}
+.field input[aria-invalid="true"], .field select[aria-invalid="true"], .field textarea[aria-invalid="true"] {
+  border-color: var(--danger);
+}
+.field-error { color: var(--danger); font-size: .82rem; line-height: 1.4; }
+.field-hint { color: var(--steel); font-size: .78rem; line-height: 1.4; }
+.form-actions { display: flex; flex-wrap: wrap; gap: .6rem; margin-top: .3rem; }
+
+.dialog-success { padding: 1.6rem 1.4rem; display: grid; gap: 1rem; }
+.dialog-success p {
+  font-size: 1.1rem; line-height: 1.5; font-weight: 600;
+  padding: 1rem 1.1rem; background: var(--accent); color: var(--accent-ink);
+  border-radius: var(--radius); border-left: 4px solid var(--accent-ink);
+}
+
+/* ---------- Responsive: >=768 ---------- */
+@media (min-width: 768px) {
+  .site-header { padding: .85rem 4vw; }
+  .nav-desktop { display: flex; }
+  .header-actions { margin-left: 1.5rem; }
+  .header-actions .btn-primary { display: inline-flex; }
+  .mobile-menu-toggle { display: none; }
+
+  .hero { padding: 5rem 4vw 4.5rem; }
+  .hero-grid { grid-template-columns: 1.1fr .9fr; gap: 3rem; }
+
+  .service-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); border: 1px solid var(--rule); }
+  .service-card { border: 0; }
+  .service-layout { grid-template-columns: 1.4fr 1fr; align-items: start; }
+
+  .principles { grid-template-columns: repeat(3, 1fr); }
+  .footer-meta { text-align: right; }
+}
+
+/* ---------- Responsive: >=1440 ---------- */
+@media (min-width: 1440px) {
+  .site-header, .hero, .services, .approach, .close, .site-footer { padding-left: max(4vw, calc((100vw - 1280px) / 2 + 2rem)); padding-right: max(4vw, calc((100vw - 1280px) / 2 + 2rem)); }
+  .service-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
 }
 
+/* ---------- Reduced motion ---------- */
 @media (prefers-reduced-motion: reduce) {
   html { scroll-behavior: auto; }
-  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
+  *, *::before, *::after {
+    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
+    transition-duration: .01ms !important; scroll-behavior: auto !important;
+  }
 }

--- baseline/src/useDisciplineParam.ts
+++ candidate/src/useDisciplineParam.ts
@@ -0,0 +1,42 @@
+import { useCallback, useEffect, useState } from 'react'
+import type { Discipline } from './content'
+
+const SUPPORTED: Discipline[] = ['all', 'electrical', 'controls', 'reliability']
+
+function readFromUrl(): Discipline {
+  if (typeof window === 'undefined') return 'all'
+  const params = new URLSearchParams(window.location.search)
+  const raw = params.get('discipline')
+  if (raw && (SUPPORTED as string[]).includes(raw)) {
+    return raw as Discipline
+  }
+  return 'all'
+}
+
+export function useDisciplineParam() {
+  const [discipline, setDisciplineState] = useState<Discipline>(() => readFromUrl())
+
+  useEffect(() => {
+    const onPop = () => setDisciplineState(readFromUrl())
+    window.addEventListener('popstate', onPop)
+    return () => window.removeEventListener('popstate', onPop)
+  }, [])
+
+  const setDiscipline = useCallback(
+    (next: Discipline) => {
+      setDisciplineState(next)
+      const params = new URLSearchParams(window.location.search)
+      if (next === 'all') {
+        params.delete('discipline')
+      } else {
+        params.set('discipline', next)
+      }
+      const qs = params.toString()
+      const url = qs ? `?${qs}` : window.location.pathname
+      window.history.pushState({}, '', url)
+    },
+    [],
+  )
+
+  return { discipline, setDiscipline }
+}
