Updated version rollout
All checks were successful
Publish / Test, build, and push image (push) Successful in 4m8s

This commit is contained in:
V 2026-05-17 13:55:41 +01:00
parent c4d199e20a
commit 7079e32a5f
14 changed files with 928 additions and 55 deletions

316
ROUND_3.md Normal file
View File

@ -0,0 +1,316 @@
# Round 3 Plan: Solo Architecture Studio Redesign
## Context
The current application is a compact server-rendered Go site with SQLite storage, Tailwind-loaded templates, HTMX navigation, image uploads, a password-protected admin area, project/gallery CRUD, a single editable `site_content` row, and a simple contact request inbox.
The public interface currently has:
- Home page with hero, intro statement, and featured projects.
- Projects index.
- Project detail pages with image overlays.
- About page that also contains the only public contact form.
- No dedicated Services page.
- No dedicated Contact page.
For a small single-person architecture practice, the redesign should not become a full agency website or heavy CMS. The site needs to build trust quickly, show a clear design position, make project work easy to inspect, and collect useful enquiries without making the owner maintain many pages.
## What From The Recommendation Fits
- Home page: relevant and already mostly present, but it needs stronger positioning, a clearer call to action, and a better preview of services/process.
- About / Studio page: highly relevant. For a solo practice, this should be one of the strongest trust-building pages because clients are hiring the person, not a large firm.
- Services page: relevant, but should be concise. A long list like architecture, interiors, renovation, consultation, space planning, furniture, project management, and 3D visualization may feel inflated for a one-person studio unless each item is genuinely offered.
- Contact page: relevant. The current form is buried on About, and the form does not pre-qualify leads.
## What To Avoid
- Do not create a large multi-page agency structure with too many thin pages.
- Do not add a complex awards/publications system unless the owner has real content to maintain.
- Do not overstate capabilities with every possible design service.
- Do not add map/social integrations until the real contact content exists.
- Do not build a full page builder. The admin should remain simple and opinionated.
## Proposed Public Site Structure
### 1. Home
Purpose: communicate the studio position quickly and send visitors to projects or enquiry.
Recommended sections:
- Full-viewport image-led hero, using a real project image when available.
- Positioning line such as "Residential architecture and interiors in London" rather than generic portfolio wording.
- One primary CTA: "Start an enquiry".
- One secondary CTA: "View projects".
- Short studio statement, 2-3 sentences.
- Featured projects, limited to 3-4.
- Compact services preview, 3 service groups maximum.
- Brief process preview, 3 steps maximum.
- Short about preview with portrait and link to Studio.
Current implementation impact:
- Keep the existing Home route.
- Rework `home.html` layout and copy fields.
- Continue using featured projects from the existing project model.
- Add admin-editable CTA text/link and positioning fields.
### 2. Studio
Purpose: make the solo practitioner credible, specific, and approachable.
This should replace or rename the current About page to "Studio". The route can remain `/about` for compatibility, but the navigation label should become `Studio`.
Recommended sections:
- Portrait or working photo.
- Name, role, and location.
- Short personal narrative.
- Design philosophy.
- Approach/process.
- Credentials, registrations, selected experience, or publications as a simple text block.
- Contact CTA.
Current implementation impact:
- Keep the existing about content fields but expand them.
- Add fields for philosophy, approach, credentials, and availability/service area.
- Remove the main contact form from this page or reduce it to a CTA linking to Contact.
### 3. Services
Purpose: help visitors self-qualify without making the practice look larger than it is.
Recommended services for a solo architecture company:
- Residential architecture.
- Renovation and extensions.
- Interior architecture and spatial planning.
- Early-stage consultation.
Optional only if accurate:
- Planning support.
- 3D visualization.
- Furniture and finish selection.
- Project coordination.
Recommended sections:
- Service overview.
- 3-4 editable service cards.
- "How projects work" process section.
- Typical timeline ranges.
- Geographic availability.
- Small FAQ.
- CTA to Contact.
Current implementation impact:
- Add `GET /services`.
- Add a `services.html` template.
- Add admin support for editable services and FAQs.
- For the first implementation, services can be stored as structured rows rather than hard-coded copy.
### 4. Contact
Purpose: create a proper conversion point and collect enough information to assess fit.
Recommended sections:
- Contact form.
- Email, phone, location.
- Short expectation statement, for example response time or preferred project types.
- Optional social links.
- Optional map later, not required now.
Inquiry questionnaire fields:
- Name.
- Email.
- Phone, optional.
- Project type.
- Location.
- Budget range.
- Timeline.
- Message.
Current implementation impact:
- Add `GET /contact`.
- Keep `POST /contact`, but expand the stored fields.
- Update admin contact inbox to show the structured enquiry details.
- Add validation for required fields and length limits.
## Proposed Navigation
Use five top-level links:
- Projects
- Studio
- Services
- Contact
Home remains the logo link. This keeps navigation focused and avoids a marketing-heavy structure.
## Backend And Data Plan
### Site Content
The existing `site_content` row is good for global editable content, but it needs more fields:
- `site_name`
- `positioning`
- `hero_cta_label`
- `hero_cta_url`
- `secondary_cta_label`
- `secondary_cta_url`
- `studio_philosophy`
- `studio_approach`
- `studio_credentials`
- `service_area`
- `response_note`
These can be added through a migration while preserving current data.
### Services
Add a `services` table:
- `id`
- `title`
- `summary`
- `details`
- `position`
- `active`
This keeps the Services page editable without turning it into a page builder.
### FAQs
Add a `faqs` table:
- `id`
- `question`
- `answer`
- `position`
- `active`
Use FAQs mainly on Services and optionally Contact.
### Contact Requests
Expand `contact_requests`:
- `phone`
- `project_type`
- `project_location`
- `budget_range`
- `timeline`
- `status`
- `notes`
`status` can start simple: `new`, `reviewed`, `archived`. `notes` is private admin-only text.
### Projects
The current project model is usable. Consider adding only:
- `summary` for cards and homepage previews.
- `scope` for project detail metadata.
- `status` or `completion_stage` if the studio wants to show built/in progress/concept.
- `position` for manual ordering.
Manual ordering matters more than created-at ordering for a portfolio.
## Admin Plan
Keep the tabbed admin from Round 2 and add two tabs:
- Main
- Projects
- Studio
- Services
- Contact
Recommended ownership:
- Main: home hero, positioning, intro, CTA labels/links, hero image.
- Projects: project CRUD, gallery uploads, featured flag, ordering.
- Studio: portrait, name, role, bio, philosophy, approach, credentials.
- Services: service rows, process copy, FAQ rows.
- Contact: public contact details, enquiry inbox, enquiry status, private notes.
The admin should stay form-based. Avoid rich text editing in this round unless there is a clear content requirement.
## Interface Direction
The current visual direction is image-led, quiet, and minimalist. That fits architecture, but the redesign should make the site feel more intentional and less like a generic template.
Recommended direction:
- Use generous image layouts for project pages.
- Keep typography restrained but improve hierarchy.
- Make the homepage less sparse by adding actionable service/process content.
- Use project facts and captions to make work inspectable.
- Keep color neutral, but avoid a flat all-neutral interface by using material-inspired accents such as stone, clay, brass, or muted green in small amounts.
- Do not use decorative cards as the main layout. Use full-width sections, grids, and strong image composition.
## Implementation Phases
### Phase 1: Public IA And Contact
- Add `/contact` page.
- Move the enquiry form there.
- Expand contact request fields.
- Add navigation item.
- Update admin contact inbox.
This creates immediate business value and fixes the weakest current conversion path.
### Phase 2: Studio And Home Rework
- Rename About navigation to Studio.
- Expand Studio content fields.
- Rework homepage sections and CTAs.
- Add home services/process preview using simple editable fields or seeded service rows.
This improves trust and positioning without a large backend expansion.
### Phase 3: Services
- Add `/services`.
- Add services and FAQs tables.
- Add admin Services tab.
- Add process/timeline/geography content.
This helps visitors self-qualify and reduces repetitive pre-sales explanations.
### Phase 4: Project Depth
- Add project summary/scope/status/manual ordering.
- Improve project detail pages with clearer metadata and richer captions.
- Add admin ordering controls.
This is valuable after the lead path and positioning are stronger.
## Testing Plan
- Add route tests for `/`, `/projects`, `/projects/{slug}`, `/about`, `/services`, and `/contact`.
- Add form tests for expanded contact validation and persistence.
- Add migration tests for new columns/tables.
- Add admin tests for new tabs and update actions.
- Manually verify HTMX navigation still works with direct URL fallback.
- Manually verify mobile layouts for Home, Studio, Services, Contact, and Project detail.
## Open Questions
- Should the public label be "Studio" while keeping `/about`, or should the route become `/studio` with `/about` redirecting?
- Which services are genuinely offered by the practitioner today?
- Does the studio want to show a phone number publicly, or keep initial contact email/form-only?
- Are there real awards, registrations, or publications to show, or should credentials remain a simple text block?
- Should contact requests trigger email notification, or is the admin inbox enough for now?
## Recommended Next Step
Start with Phase 1. A dedicated Contact page with a structured enquiry form is the highest-value backend and interface improvement, and it gives the later Home, Studio, and Services pages a clear conversion destination.

244
ROUND_4.md Normal file
View File

@ -0,0 +1,244 @@
# Round 4 Plan: Public Shell, HTMX Partials, and Smoother Navigation
## Goal
Refactor the public interface so `base.html` is the single public shell for header, footer, DaisyUI drawer, overlay root, and the stable main content container. Public pages should render either as a full document on direct visits or as main-content partials during HTMX navigation.
This keeps the current architecture portfolio style and functionality while making page structure easier to maintain.
## Current State
- Public pages call `{{template "site_start" .}}`, then render their own `<main>`, then call `{{template "site_end" .}}`.
- Public navigation generally uses `hx-boost="true"` with `hx-target="body"` and full `body` replacement.
- The mobile sidebar now uses DaisyUI drawer structure.
- Admin already has a better partial pattern: full page for direct requests, partial panel for HTMX requests, plus out-of-band tab updates.
## Target Structure
Use `base.html` as the public shell.
Recommended template ownership:
- `base.html`
- `head`
- full public document shell
- header
- desktop nav
- DaisyUI mobile drawer
- footer
- `#main-content`
- `#overlay-root`
- OOB nav fragments
- Page templates
- define only page content partials, for example:
- `home_content`
- `projects_content`
- `project_content`
- `studio_content`
- `services_content`
- `contact_content`
## Rendering Model
### Direct Page Load
Normal browser requests render the full shell:
```html
<!doctype html>
<html>
<head>...</head>
<body>
<div class="drawer drawer-end">
...
<main id="main-content">
page content
</main>
...
</div>
</body>
</html>
```
### HTMX Navigation
HTMX requests return:
- the new `#main-content` element
- out-of-band desktop nav active state
- out-of-band drawer nav active state
- optionally an out-of-band document title update if needed later
This mirrors the admin partial strategy.
## HTMX Navigation Changes
Change public internal navigation from body replacement:
```html
hx-target="body"
hx-swap="outerHTML transition:true"
```
to main-content replacement:
```html
hx-target="#main-content"
hx-swap="outerHTML transition:true"
hx-push-url="true"
```
The header, drawer, footer, and overlay root remain stable across page transitions.
## Active Navigation
Because only the main content will swap, active navigation state needs to update separately.
Use out-of-band fragments:
- `#site-desktop-nav`
- `#site-drawer-nav`
The full shell and HTMX partial responses both render nav from the same template definitions, avoiding duplicated active-state logic.
## Drawer Behavior
Keep the DaisyUI drawer:
- `drawer drawer-end`
- `drawer-toggle`
- `drawer-content`
- `drawer-side`
- `drawer-overlay`
Improvements:
- Keep the drawer as part of the stable shell, not page content.
- Close drawer after clicking a drawer nav link.
- Close drawer on Escape.
- Keep the matted glass visual:
- `bg-neutral-950/45`
- `backdrop-blur-xl`
- white text
- active item underlined
## Smoother Transitions
### Page Content
Apply transitions to `#main-content`, not the full body.
Recommended CSS:
- old content: slight fade out and 4-6px downward motion
- new content: fade in and return to zero offset
- short duration, around 160-220ms
- respect `prefers-reduced-motion`
The existing View Transition CSS can be adjusted so:
- `#main-content` has `view-transition-name: main-content`
- root transitions are less dominant or removed for public navigation
- admin panel transitions remain independent
### Drawer
DaisyUI handles the structural entry/exit, but add a small custom polish layer if needed:
- slightly longer transform duration, around 240ms
- eased slide-in/out
- backdrop fade around 180-220ms
Avoid custom JavaScript animation. Prefer CSS.
## Backend Changes
Add a public render helper similar to admin rendering:
```go
func (s *Server) renderPublic(w http.ResponseWriter, r *http.Request, fullTemplate, partialTemplate string, data pageData)
```
Behavior:
- normal request: render full shell with the correct page content
- HTMX request: render partial content plus OOB nav fragments
Each public handler should call `renderPublic` instead of `render`.
## Template Naming Plan
Suggested full/partial template names:
- `home.html` full shell, `home_partial.html`
- `projects.html` full shell, `projects_partial.html`
- `project.html` full shell, `project_partial.html`
- `about.html` full shell, `about_partial.html`
- `services.html` full shell, `services_partial.html`
- `contact.html` full shell, `contact_partial.html`
Alternatively, each page file can define:
- `page_full`
- `page_partial`
- `page_content`
Pick one convention and use it everywhere.
## Preserve Current Functionality
Must continue working:
- direct URL visits
- browser back/forward with pushed URLs
- non-JavaScript navigation fallback
- project image overlay HTMX requests
- contact form HTMX submission
- admin tab partial behavior
- health endpoints
- DaisyUI mobile drawer
## Testing Plan
Update or add tests for:
- direct public routes return full HTML document
- HTMX public route requests return partial content, not full document
- HTMX public partial responses include OOB nav fragments
- active nav state updates for each page
- project image overlay still returns only overlay fragment
- contact form submission still returns only `contact_result.html`
- existing admin HTMX tests still pass
Manual checks:
- mobile drawer opens/closes on every public page
- drawer closes after nav click
- drawer closes with Escape
- page transitions feel smooth on desktop and mobile
- reduced-motion setting disables meaningful animation
- browser back/forward updates main content and nav state
## Implementation Order
1. Refactor `base.html` into the full public shell.
2. Convert one page, preferably Home, to full/partial/content templates.
3. Add `renderPublic`.
4. Convert remaining public pages.
5. Add OOB nav fragments.
6. Update HTMX targets in header, drawer, and internal CTAs.
7. Adjust transition CSS for `#main-content`.
8. Add drawer close-on-link/Escape behavior.
9. Update tests.
10. Run full test suite and manually verify key routes.
## Risks
- OOB nav fragments can become noisy if duplicated in every page template. Keep them in `base.html`.
- Contact form and overlay requests should not use `renderPublic`; they should keep returning small fragments.
- Page title will not automatically update if only `#main-content` swaps. This can be handled later with an OOB `<title>` strategy or small JS.
- DaisyUI CDN use is acceptable for now, but a proper Tailwind/DaisyUI build pipeline would be better for production performance and version control.
## Recommended Scope For Round 4
Implement the public shell and main-content HTMX navigation first. Do not refactor admin forms or public content sections in the same round. Keep this round focused on layout architecture, navigation maintainability, and smoother transitions.

View File

@ -19,7 +19,7 @@ func (s *Server) home(w http.ResponseWriter, r *http.Request) {
s.error(w, err)
return
}
s.render(w, "home.html", pageData{Title: content.HeroTitle, Active: "home", Content: content, Projects: projects, CurrentPath: r.URL.Path})
s.renderPublic(w, r, "home.html", "home_partial.html", pageData{Title: content.HeroTitle, Active: "home", Content: content, Projects: projects, CurrentPath: r.URL.Path})
}
func (s *Server) projects(w http.ResponseWriter, r *http.Request) {
@ -33,7 +33,7 @@ func (s *Server) projects(w http.ResponseWriter, r *http.Request) {
s.error(w, err)
return
}
s.render(w, "projects.html", pageData{Title: "Projects", Active: "projects", Content: content, Projects: projects, CurrentPath: r.URL.Path})
s.renderPublic(w, r, "projects.html", "projects_partial.html", pageData{Title: "Projects", Active: "projects", Content: content, Projects: projects, CurrentPath: r.URL.Path})
}
func (s *Server) projectDetail(w http.ResponseWriter, r *http.Request) {
@ -51,7 +51,7 @@ func (s *Server) projectDetail(w http.ResponseWriter, r *http.Request) {
s.error(w, err)
return
}
s.render(w, "project.html", pageData{Title: project.Title, Active: "projects", Content: content, Project: project, CurrentPath: r.URL.Path})
s.renderPublic(w, r, "project.html", "project_partial.html", pageData{Title: project.Title, Active: "projects", Content: content, Project: project, CurrentPath: r.URL.Path})
}
func (s *Server) projectImageOverlay(w http.ResponseWriter, r *http.Request) {
@ -78,7 +78,7 @@ func (s *Server) about(w http.ResponseWriter, r *http.Request) {
s.error(w, err)
return
}
s.render(w, "about.html", pageData{Title: "Studio", Active: "about", Content: content, CurrentPath: r.URL.Path})
s.renderPublic(w, r, "about.html", "about_partial.html", pageData{Title: "Studio", Active: "about", Content: content, CurrentPath: r.URL.Path})
}
func (s *Server) services(w http.ResponseWriter, r *http.Request) {
@ -97,7 +97,7 @@ func (s *Server) services(w http.ResponseWriter, r *http.Request) {
s.error(w, err)
return
}
s.render(w, "services.html", pageData{Title: "Services", Active: "services", Content: content, Services: services, FAQs: faqs, CurrentPath: r.URL.Path})
s.renderPublic(w, r, "services.html", "services_partial.html", pageData{Title: "Services", Active: "services", Content: content, Services: services, FAQs: faqs, CurrentPath: r.URL.Path})
}
func (s *Server) contactPage(w http.ResponseWriter, r *http.Request) {
@ -106,7 +106,7 @@ func (s *Server) contactPage(w http.ResponseWriter, r *http.Request) {
s.error(w, err)
return
}
s.render(w, "contact.html", pageData{Title: "Contact", Active: "contact", Content: content, CurrentPath: r.URL.Path})
s.renderPublic(w, r, "contact.html", "contact_partial.html", pageData{Title: "Contact", Active: "contact", Content: content, CurrentPath: r.URL.Path})
}
func (s *Server) contact(w http.ResponseWriter, r *http.Request) {

View File

@ -24,6 +24,48 @@ func TestPublicRoutes(t *testing.T) {
}
}
func TestPublicRoutesRenderFullShellOnDirectLoad(t *testing.T) {
srv := newTestServer(t)
req := httptest.NewRequest(http.MethodGet, "/services", nil)
rec := httptest.NewRecorder()
srv.Routes().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected ok, got %d", rec.Code)
}
body, _ := io.ReadAll(rec.Result().Body)
text := string(body)
for _, want := range []string{"<!doctype html>", `class="drawer drawer-end"`, `id="main-content"`, `id="site-drawer"`} {
if !strings.Contains(text, want) {
t.Fatalf("direct public route missing %q: %s", want, text)
}
}
}
func TestPublicHTMXRoutesReturnMainContentPartial(t *testing.T) {
srv := newTestServer(t)
req := httptest.NewRequest(http.MethodGet, "/services", nil)
req.Header.Set("HX-Request", "true")
rec := httptest.NewRecorder()
srv.Routes().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected ok, got %d", rec.Code)
}
body, _ := io.ReadAll(rec.Result().Body)
text := string(body)
if strings.Contains(text, "<!doctype html>") || strings.Contains(text, `class="drawer drawer-end"`) {
t.Fatalf("expected partial response, got full document: %s", text)
}
for _, want := range []string{`id="main-content"`, `id="site-header"`, `id="site-drawer-nav"`, `hx-swap-oob="true"`} {
if !strings.Contains(text, want) {
t.Fatalf("HTMX public route missing %q: %s", want, text)
}
}
}
func TestServicesRouteRendersServicesAndFAQs(t *testing.T) {
srv := newTestServer(t)
req := httptest.NewRequest(http.MethodGet, "/services", nil)

View File

@ -25,6 +25,14 @@ func (s *Server) renderAdmin(w http.ResponseWriter, r *http.Request, fullTemplat
s.render(w, fullTemplate, data)
}
func (s *Server) renderPublic(w http.ResponseWriter, r *http.Request, fullTemplate, partialTemplate string, data pageData) {
if r.Header.Get("HX-Request") == "true" {
s.render(w, partialTemplate, data)
return
}
s.render(w, fullTemplate, data)
}
func (s *Server) error(w http.ResponseWriter, err error) {
http.Error(w, err.Error(), http.StatusInternalServerError)
}

View File

@ -8,6 +8,31 @@
updateHeader();
window.addEventListener("scroll", updateHeader, { passive: true });
function currentTheme() {
try {
const saved = localStorage.getItem("archi-theme");
if (saved === "dark" || saved === "light") return saved;
} catch (_) {}
return document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light";
}
function setTheme(theme) {
const normalized = theme === "dark" ? "dark" : "light";
document.documentElement.setAttribute("data-theme", normalized);
document.querySelectorAll("[data-theme-toggle]").forEach(function (input) {
input.checked = normalized === "dark";
});
try {
localStorage.setItem("archi-theme", normalized);
} catch (_) {}
}
function syncThemeControls() {
setTheme(currentTheme());
}
syncThemeControls();
function closeOverlay() {
const root = document.getElementById("overlay-root");
if (root) root.innerHTML = "";
@ -25,13 +50,25 @@
});
document.addEventListener("htmx:afterSettle", updateHeader);
document.addEventListener("htmx:afterSettle", syncThemeControls);
document.addEventListener("click", function (event) {
if (event.target.closest("[data-drawer-link]")) {
const drawer = document.getElementById("site-drawer");
if (drawer) drawer.checked = false;
}
if (event.target.matches("[data-overlay], [data-overlay-close]")) {
closeOverlay();
}
});
document.addEventListener("change", function (event) {
const toggle = event.target.closest("[data-theme-toggle]");
if (toggle) {
setTheme(toggle.checked ? "dark" : "light");
}
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
const drawer = document.getElementById("site-drawer");

View File

@ -6,6 +6,61 @@ body {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
html[data-theme="dark"] body,
html[data-theme="dark"] .drawer-content {
background: #0f0f0e;
color: #f5f5f4;
}
html[data-theme="dark"] .bg-neutral-50 {
background-color: #0f0f0e !important;
}
html[data-theme="dark"] .bg-white {
background-color: #171716 !important;
}
html[data-theme="dark"] .bg-neutral-950 {
background-color: #050505 !important;
}
html[data-theme="dark"] .bg-neutral-200 {
background-color: #2a2926 !important;
}
html[data-theme="dark"] .text-neutral-950,
html[data-theme="dark"] .text-neutral-800 {
color: #f5f5f4 !important;
}
html[data-theme="dark"] .text-neutral-700,
html[data-theme="dark"] .text-neutral-600,
html[data-theme="dark"] .text-neutral-500 {
color: #c7c2b8 !important;
}
html[data-theme="dark"] .text-neutral-400 {
color: #a8a29a !important;
}
html[data-theme="dark"] .border-neutral-300,
html[data-theme="dark"] .border-neutral-200 {
border-color: rgba(245, 245, 244, 0.16) !important;
}
html[data-theme="dark"] input,
html[data-theme="dark"] textarea,
html[data-theme="dark"] select {
background-color: #111110;
border-color: rgba(245, 245, 244, 0.22);
color: #f5f5f4;
}
html[data-theme="dark"] input::placeholder,
html[data-theme="dark"] textarea::placeholder {
color: #78716c;
}
.form-select {
appearance: none;
border-radius: 0;
@ -30,6 +85,32 @@ body {
font-size: 1.25rem;
}
html[data-theme="dark"] [data-site-header] {
background: rgba(15, 15, 14, 0.95) !important;
color: #f5f5f4 !important;
box-shadow: 0 1px 0 rgba(245, 245, 244, 0.12);
backdrop-filter: blur(18px);
}
html[data-theme="dark"] [data-site-header].is-compact {
background: rgba(15, 15, 14, 0.95) !important;
color: #f5f5f4 !important;
box-shadow: 0 1px 0 rgba(245, 245, 244, 0.12);
}
#main-content {
view-transition-name: main-content;
}
.drawer-side > aside {
transition-duration: 260ms;
transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
.drawer-overlay {
transition-duration: 200ms;
}
@keyframes page-fade-out {
from {
opacity: 1;
@ -90,6 +171,14 @@ body {
animation: 180ms ease both panel-fade-in;
}
::view-transition-old(main-content) {
animation: 150ms ease both panel-fade-out;
}
::view-transition-new(main-content) {
animation: 210ms ease both panel-fade-in;
}
@media (prefers-reduced-motion: reduce) {
*,
::before,
@ -99,6 +188,8 @@ body {
::view-transition-old(root),
::view-transition-new(root),
::view-transition-old(main-content),
::view-transition-new(main-content),
::view-transition-old(admin-panel),
::view-transition-new(admin-panel) {
animation-duration: 1ms;

View File

@ -1,5 +1,16 @@
{{template "site_start" .}}
<main class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
{{define "about.html"}}
{{template "public_shell_start" .}}
{{template "about_content" .}}
{{template "public_shell_end" .}}
{{end}}
{{define "about_partial.html"}}
{{template "public_nav_oob" .}}
{{template "about_content" .}}
{{end}}
{{define "about_content"}}
<main id="main-content" class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
<section class="grid gap-10 md:grid-cols-[0.9fr_1.1fr] md:items-start">
<div class="aspect-[4/5] overflow-hidden bg-neutral-200">
<img src="{{.Content.AboutImage}}" alt="{{.Content.AboutName}}" class="h-full w-full object-cover">
@ -60,9 +71,9 @@
<h2 class="text-3xl font-semibold">Discuss a project</h2>
<div class="max-w-2xl">
<p class="text-lg leading-relaxed text-neutral-600">Share the project type, location, budget range, and timeline so the studio can assess whether the work is a good fit.</p>
<a href="/contact" class="mt-6 inline-flex bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">Start an enquiry</a>
<a href="/contact" class="mt-6 inline-flex bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">Start an enquiry</a>
</div>
</div>
</section>
</main>
{{template "site_end" .}}
{{end}}

View File

@ -5,6 +5,16 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{if .Title}}{{.Title}} | {{end}}Archi Folio</title>
<script>
(function () {
try {
var theme = localStorage.getItem("archi-theme");
if (theme === "dark" || theme === "light") {
document.documentElement.setAttribute("data-theme", theme);
}
} catch (_) {}
})();
</script>
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
@ -14,7 +24,7 @@
<body class="bg-neutral-50 text-neutral-950 antialiased">
{{end}}
{{define "site_start"}}
{{define "public_shell_start"}}
{{template "head" .}}
<div class="drawer drawer-end">
<input id="site-drawer" type="checkbox" class="drawer-toggle">
@ -23,15 +33,24 @@
{{end}}
{{define "site_header"}}
<header data-site-header class="fixed inset-x-0 top-0 z-40 transition-all duration-300 {{if eq .Active "home"}}py-7 text-white{{else}}bg-neutral-50/95 py-4 shadow-sm backdrop-blur text-neutral-950{{end}}">
<header id="site-header" data-site-header class="fixed inset-x-0 top-0 z-40 transition-all duration-300 {{if eq .Active "home"}}py-7 text-white{{else}}bg-neutral-50/95 py-4 shadow-sm backdrop-blur text-neutral-950{{end}}">
{{template "site_header_inner" .}}
</header>
{{end}}
{{define "site_header_oob"}}
<header id="site-header" data-site-header class="fixed inset-x-0 top-0 z-40 transition-all duration-300 {{if eq .Active "home"}}py-7 text-white{{else}}bg-neutral-50/95 py-4 shadow-sm backdrop-blur text-neutral-950{{end}}" hx-swap-oob="true">
{{template "site_header_inner" .}}
</header>
{{end}}
{{define "site_header_inner"}}
<div class="mx-auto flex max-w-7xl items-center justify-between px-5 md:px-8">
<a href="/" class="text-xl font-semibold tracking-normal md:text-3xl" data-header-brand hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">Archi Folio</a>
<nav class="hidden items-center gap-4 text-sm uppercase tracking-[0.18em] md:flex md:gap-8" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">
<a class="hover:opacity-60 {{if eq .Active "projects"}}font-semibold{{end}}" href="/projects">Projects</a>
<a class="hover:opacity-60 {{if eq .Active "about"}}font-semibold{{end}}" href="/about">Studio</a>
<a class="hover:opacity-60 {{if eq .Active "services"}}font-semibold{{end}}" href="/services">Services</a>
<a class="hover:opacity-60 {{if eq .Active "contact"}}font-semibold{{end}}" href="/contact">Contact</a>
</nav>
<a href="/" class="text-xl font-semibold tracking-normal md:text-3xl" data-header-brand hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">Archi Folio</a>
<div class="hidden items-center gap-6 md:flex">
{{template "site_desktop_nav" .}}
{{template "theme_toggle" .}}
</div>
<label for="site-drawer" class="grid h-11 w-11 cursor-pointer place-items-center border border-current/30 md:hidden" aria-label="Open menu">
<span class="grid gap-1.5">
<span class="block h-px w-5 bg-current"></span>
@ -40,7 +59,24 @@
</span>
</label>
</div>
</header>
{{end}}
{{define "site_desktop_nav"}}
<nav id="site-desktop-nav" class="hidden items-center gap-4 text-sm uppercase tracking-[0.18em] md:flex md:gap-8" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">
<a class="hover:opacity-60 {{if eq .Active "projects"}}font-semibold{{end}}" href="/projects">Projects</a>
<a class="hover:opacity-60 {{if eq .Active "about"}}font-semibold{{end}}" href="/about">Studio</a>
<a class="hover:opacity-60 {{if eq .Active "services"}}font-semibold{{end}}" href="/services">Services</a>
<a class="hover:opacity-60 {{if eq .Active "contact"}}font-semibold{{end}}" href="/contact">Contact</a>
</nav>
{{end}}
{{define "site_desktop_nav_oob"}}
<nav id="site-desktop-nav" class="hidden items-center gap-4 text-sm uppercase tracking-[0.18em] md:flex md:gap-8" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true" hx-swap-oob="true">
<a class="hover:opacity-60 {{if eq .Active "projects"}}font-semibold{{end}}" href="/projects">Projects</a>
<a class="hover:opacity-60 {{if eq .Active "about"}}font-semibold{{end}}" href="/about">Studio</a>
<a class="hover:opacity-60 {{if eq .Active "services"}}font-semibold{{end}}" href="/services">Services</a>
<a class="hover:opacity-60 {{if eq .Active "contact"}}font-semibold{{end}}" href="/contact">Contact</a>
</nav>
{{end}}
{{define "footer"}}
@ -52,7 +88,24 @@
</footer>
{{end}}
{{define "site_end"}}
{{define "theme_toggle"}}
<label class="swap swap-rotate grid h-10 w-10 cursor-pointer place-items-center text-current" aria-label="Toggle dark theme">
<input type="checkbox" value="dark" class="theme-controller" data-theme-toggle>
<svg class="swap-off h-6 w-6 fill-current" aria-label="sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z"></path>
</svg>
<svg class="swap-on h-6 w-6 fill-current" aria-label="moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z"></path>
</svg>
</label>
{{end}}
{{define "public_nav_oob"}}
{{template "site_header_oob" .}}
{{template "site_drawer_nav_oob" .}}
{{end}}
{{define "public_shell_end"}}
{{template "footer" .}}
<div id="overlay-root"></div>
</div>
@ -60,20 +113,18 @@
<label for="site-drawer" aria-label="Close menu" class="drawer-overlay bg-neutral-950/45"></label>
<aside class="flex min-h-full w-[min(22rem,86vw)] flex-col bg-neutral-950/45 text-white shadow-2xl backdrop-blur-xl">
<div class="flex items-center justify-between border-b border-white/15 px-5 py-4">
<a href="/" class="text-xl font-semibold text-white" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">Archi Folio</a>
<label for="site-drawer" class="grid h-10 w-10 cursor-pointer place-items-center border border-white/25 text-white" aria-label="Close menu">
<span class="relative block h-5 w-5">
<span class="absolute left-0 top-1/2 block h-px w-5 rotate-45 bg-current"></span>
<span class="absolute left-0 top-1/2 block h-px w-5 -rotate-45 bg-current"></span>
</span>
</label>
<a href="/" class="text-xl font-semibold text-white" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true" data-drawer-link>Archi Folio</a>
<div class="flex items-center gap-3">
{{template "theme_toggle" .}}
<label for="site-drawer" class="grid h-10 w-10 cursor-pointer place-items-center border border-white/25 text-white" aria-label="Close menu">
<span class="relative block h-5 w-5">
<span class="absolute left-0 top-1/2 block h-px w-5 rotate-45 bg-current"></span>
<span class="absolute left-0 top-1/2 block h-px w-5 -rotate-45 bg-current"></span>
</span>
</label>
</div>
</div>
<nav class="grid px-5 py-6 text-2xl font-medium text-white" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "projects"}}underline underline-offset-8{{end}}" href="/projects">Projects</a>
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "about"}}underline underline-offset-8{{end}}" href="/about">Studio</a>
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "services"}}underline underline-offset-8{{end}}" href="/services">Services</a>
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "contact"}}underline underline-offset-8{{end}}" href="/contact">Contact</a>
</nav>
{{template "site_drawer_nav" .}}
<div class="mt-auto px-5 pb-6 text-sm leading-relaxed text-white">
<p>{{.Content.Email}}</p>
<p>{{.Content.Location}}</p>
@ -84,3 +135,21 @@
</body>
</html>
{{end}}
{{define "site_drawer_nav"}}
<nav id="site-drawer-nav" class="grid px-5 py-6 text-2xl font-medium text-white" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "projects"}}underline underline-offset-8{{end}}" href="/projects" data-drawer-link>Projects</a>
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "about"}}underline underline-offset-8{{end}}" href="/about" data-drawer-link>Studio</a>
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "services"}}underline underline-offset-8{{end}}" href="/services" data-drawer-link>Services</a>
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "contact"}}underline underline-offset-8{{end}}" href="/contact" data-drawer-link>Contact</a>
</nav>
{{end}}
{{define "site_drawer_nav_oob"}}
<nav id="site-drawer-nav" class="grid px-5 py-6 text-2xl font-medium text-white" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true" hx-swap-oob="true">
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "projects"}}underline underline-offset-8{{end}}" href="/projects" data-drawer-link>Projects</a>
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "about"}}underline underline-offset-8{{end}}" href="/about" data-drawer-link>Studio</a>
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "services"}}underline underline-offset-8{{end}}" href="/services" data-drawer-link>Services</a>
<a class="border-b border-white/15 py-4 text-white {{if eq .Active "contact"}}underline underline-offset-8{{end}}" href="/contact" data-drawer-link>Contact</a>
</nav>
{{end}}

View File

@ -1,5 +1,16 @@
{{template "site_start" .}}
<main class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
{{define "contact.html"}}
{{template "public_shell_start" .}}
{{template "contact_content" .}}
{{template "public_shell_end" .}}
{{end}}
{{define "contact_partial.html"}}
{{template "public_nav_oob" .}}
{{template "contact_content" .}}
{{end}}
{{define "contact_content"}}
<main id="main-content" class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
<section class="grid gap-10 border-b border-neutral-200 pb-14 md:grid-cols-[0.85fr_1.15fr] md:items-end">
<div>
<p class="mb-3 text-sm uppercase tracking-[0.2em] text-neutral-500">Project enquiry</p>
@ -87,4 +98,4 @@
</form>
</section>
</main>
{{template "site_end" .}}
{{end}}

View File

@ -1,5 +1,16 @@
{{template "site_start" .}}
<main>
{{define "home.html"}}
{{template "public_shell_start" .}}
{{template "home_content" .}}
{{template "public_shell_end" .}}
{{end}}
{{define "home_partial.html"}}
{{template "public_nav_oob" .}}
{{template "home_content" .}}
{{end}}
{{define "home_content"}}
<main id="main-content">
<section class="relative min-h-[88vh] overflow-hidden bg-neutral-950">
<img src="{{.Content.HeroImage}}" alt="" class="absolute inset-0 h-full w-full object-cover opacity-90">
<div class="absolute inset-0 bg-gradient-to-b from-black/40 via-black/10 to-black/65"></div>
@ -7,7 +18,7 @@
<p class="mb-4 max-w-xl text-sm uppercase tracking-[0.22em] text-white/75">{{.Content.Positioning}}</p>
<h1 class="max-w-5xl text-5xl font-semibold leading-[0.95] md:text-8xl">{{.Content.HeroTitle}}</h1>
<p class="mt-6 max-w-2xl text-lg leading-relaxed text-white/80 md:text-xl">{{.Content.HeroSubtitle}}</p>
<div class="mt-8 flex flex-wrap gap-3" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">
<div class="mt-8 flex flex-wrap gap-3" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">
<a href="{{.Content.HeroCTAURL}}" class="bg-white px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-neutral-950 hover:bg-neutral-200">{{.Content.HeroCTALabel}}</a>
<a href="{{.Content.SecondaryCTAURL}}" class="border border-white/60 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-white hover:text-neutral-950">{{.Content.SecondaryCTALabel}}</a>
</div>
@ -18,7 +29,7 @@
<h2 class="text-3xl font-semibold md:text-5xl">{{.Content.IntroTitle}}</h2>
<div class="max-w-2xl">
<p class="text-xl leading-relaxed text-neutral-600">{{.Content.IntroText}}</p>
<a href="/about" class="mt-6 inline-flex text-sm uppercase tracking-[0.18em] text-neutral-500 hover:text-neutral-950" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">Meet the studio</a>
<a href="/about" class="mt-6 inline-flex text-sm uppercase tracking-[0.18em] text-neutral-500 hover:text-neutral-950" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">Meet the studio</a>
</div>
</section>
@ -45,7 +56,7 @@
</div>
</section>
<section class="px-5 py-20 md:px-8 md:py-28" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">
<section class="px-5 py-20 md:px-8 md:py-28" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">
<div class="mx-auto mb-8 flex max-w-7xl items-end justify-between">
<h2 class="text-2xl font-semibold md:text-4xl">Featured Projects</h2>
<a href="/projects" class="text-sm uppercase tracking-[0.18em] text-neutral-500 hover:text-neutral-950">All work</a>
@ -103,8 +114,8 @@
<p class="mb-3 text-sm uppercase tracking-[0.2em] text-neutral-500">{{.Content.AboutRole}}</p>
<h2 class="text-4xl font-semibold md:text-6xl">{{.Content.AboutName}}</h2>
<p class="mt-6 max-w-2xl text-xl leading-relaxed text-neutral-600">{{.Content.AboutBio}}</p>
<a href="/about" class="mt-8 inline-flex bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">Studio profile</a>
<a href="/about" class="mt-8 inline-flex bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">Studio profile</a>
</div>
</section>
</main>
{{template "site_end" .}}
{{end}}

View File

@ -1,5 +1,16 @@
{{template "site_start" .}}
<main class="pt-28 md:pt-36">
{{define "project.html"}}
{{template "public_shell_start" .}}
{{template "project_content" .}}
{{template "public_shell_end" .}}
{{end}}
{{define "project_partial.html"}}
{{template "public_nav_oob" .}}
{{template "project_content" .}}
{{end}}
{{define "project_content"}}
<main id="main-content" class="pt-28 md:pt-36">
<section class="mx-auto max-w-7xl px-5 md:px-8">
<p class="mb-4 text-sm uppercase tracking-[0.2em] text-neutral-500">{{.Project.Category}} · {{.Project.Status}}</p>
<div class="grid gap-8 md:grid-cols-[1.2fr_0.8fr] md:items-end">
@ -34,4 +45,4 @@
</div>
</section>
</main>
{{template "site_end" .}}
{{end}}

View File

@ -1,5 +1,16 @@
{{template "site_start" .}}
<main class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
{{define "projects.html"}}
{{template "public_shell_start" .}}
{{template "projects_content" .}}
{{template "public_shell_end" .}}
{{end}}
{{define "projects_partial.html"}}
{{template "public_nav_oob" .}}
{{template "projects_content" .}}
{{end}}
{{define "projects_content"}}
<main id="main-content" class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
<div class="mb-10 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<p class="mb-3 text-sm uppercase tracking-[0.2em] text-neutral-500">Selected work</p>
@ -7,7 +18,7 @@
</div>
<p class="max-w-md text-neutral-600">A visual index of architectural and interior design work.</p>
</div>
<div class="grid gap-x-5 gap-y-10 sm:grid-cols-2 lg:grid-cols-3" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">
<div class="grid gap-x-5 gap-y-10 sm:grid-cols-2 lg:grid-cols-3" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">
{{range .Projects}}
<a href="/projects/{{.Slug}}" class="group block">
<div class="aspect-[4/3] overflow-hidden bg-neutral-200">
@ -25,4 +36,4 @@
{{end}}
</div>
</main>
{{template "site_end" .}}
{{end}}

View File

@ -1,5 +1,16 @@
{{template "site_start" .}}
<main class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
{{define "services.html"}}
{{template "public_shell_start" .}}
{{template "services_content" .}}
{{template "public_shell_end" .}}
{{end}}
{{define "services_partial.html"}}
{{template "public_nav_oob" .}}
{{template "services_content" .}}
{{end}}
{{define "services_content"}}
<main id="main-content" class="mx-auto max-w-7xl px-5 pb-24 pt-32 md:px-8 md:pt-40">
<section class="grid gap-10 border-b border-neutral-200 pb-14 md:grid-cols-[0.82fr_1.18fr] md:items-end">
<div>
<p class="mb-3 text-sm uppercase tracking-[0.2em] text-neutral-500">{{.Content.ServiceArea}}</p>
@ -64,8 +75,8 @@
<h2 class="text-3xl font-semibold">Ready to discuss a project?</h2>
<div>
<p class="max-w-2xl text-lg leading-relaxed text-neutral-600">Use the enquiry form to share the project type, site location, budget range, and timeline.</p>
<a href="/contact" class="mt-6 inline-flex bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700" hx-boost="true" hx-target="body" hx-swap="outerHTML transition:true">Start an enquiry</a>
<a href="/contact" class="mt-6 inline-flex bg-neutral-950 px-5 py-3 text-sm font-medium uppercase tracking-[0.18em] text-white hover:bg-neutral-700" hx-boost="true" hx-target="#main-content" hx-swap="outerHTML transition:true" hx-push-url="true">Start an enquiry</a>
</div>
</section>
</main>
{{template "site_end" .}}
{{end}}