README Reference

Quantalumin Hugo Theme

Theme for the Hugo static website generator, for scientific and pedagogical applications.

Documentation Map

For the generator as a system, start with:

The generator also now serves these docs as a Hugo documentation website from:

  • /
  • /docs/
  • /docs/reference/

Run the documentation website locally with:

  • npm run docs:server

Build the documentation website to disk with:

  • npm run docs:build

The docs website uses the dedicated config profile in:

  • config/docs-site/config.yaml

The shortcode manifest is intended to become the MCP-facing inventory for agents editing websites.

Features

Container Boundary

The shared Hugo/editor theme is not itself a hosted K3s service.

The only containerized runtime in this repo is the local markdown bridge used by the editor:

  • Containerfile.local-bridge
  • container/README.md

Keep that bridge local-only. Hosted generator runtime belongs in generator-api, not in the theme repo.

Canonical Root Tokens

The theme now emits a canonical :root token surface from params.style.tokens.* in assets/css/variables.css.

Current emitted groups:

  • params.style.tokens.color.* -> --color-*
  • params.style.tokens.space.* -> --space-*
  • params.style.tokens.radius.* -> --radius-*
  • params.style.tokens.shadow.* -> --shadow-*
  • params.style.tokens.section.* -> --section-*
  • params.style.tokens.region.* -> --region-*
  • params.style.tokens.width.* -> width-oriented names such as --content-max-width, --section-max-width, --media-width, --aside-width
  • params.style.tokens.type.* -> --font-*, --text-size-*, --line-height-* where applicable

Legacy site.Params.style.* variables are still emitted too, so migration can happen incrementally rather than as a flag day.

The first scoped-surface bridge is now live too: assets/css/variables.css rebinds the shared palette names --bg, --fg, --fg-bold, --fg-light, --border, --accent, --hover-accent, --icon, and --fig-bg on header, footer, aside, and #article-nav. New consumers should prefer those shared scoped names and only fall back to legacy header_* / footer_* / aside_* / article_nav_* variables while the migration is still in progress. Those scoped rebindings must use non-cyclic root fallbacks like --surface-root-bg, not self-referential patterns like --bg: var(--footer_bg, var(--bg)), because Firefox drops those cycles.

Scoped palette surfaces keep a readability fallback too: if a site explicitly sets a dark footer_bg equal to the global invert_bg but does not provide matching footer_fg / footer_icon values, the footer inherits the invert foreground/icon/border family by default instead of reusing the normal light-surface text colors.

Widgets are part of the same scoped-token architecture, not exceptions to it. The site theme should own the base tokens, each widget root should alias them into local --widget-* variables (or widget-family aliases like --booking-*), then the widget should reset global component leakage such as shared button styles and rebuild its own controls explicitly with flex/grid and explicit spacing. Widget internals should consume only those local aliases, so each site inherits branding automatically and can override the widget safely without forking shared SCSS. See widget-integration-plan.md.

Sites can now separate their general accent from their interaction/hover accent through hover_accent and scoped overrides such as footer_hover_accent. Shared hover consumers like nav links, ordinary links, buttons, and icons should prefer --hover-accent with --accent as the fallback.

Normal footer rendering should still consume the scoped footer_* tokens directly. Footer icons/titles/borders should not read footer_invert_* unless a footer-specific invert/glass treatment is explicitly intended.

Head Loading Guardrails

The shared head pipeline should have exactly two authoritative entrypoints:

  • layouts/partials/head/css.html for stylesheet linking
  • layouts/partials/head/import/runtime-core.html for the critical JS/editor runtime chain

Site overrides should not hand-roll the core runtime tags for prefetch, Vim, cursor presence, Nowtype, or edit mode. Keep those imports routed through runtime-core and extend via site head/import/*.html overrides instead.

Guardrails:

  • use the shared .CDN import context for asset URLs; do not hardcode mixed /cdn/... and helper-built paths inside the same chain
  • register shared optional CDN assets through params.cdnAssets in the generator config rather than hardcoding one-off <script>/<link> tags in site head forks
  • the canonical shared asset source is now cdn/; do not place shared CDN/runtime assets under theme static/
  • keep cdn/editor/ trimmed to runtime/source files; do not leave nested git metadata, node_modules/, or packaged Electron outputs there
  • local hugo server now resolves shared assets through params.devCdnBase (falling back to params.cdnBase); use that instead of expecting a theme-local /cdn/ tree
  • in local development, head/css.html should link /css/style.css and /css/brochure.css directly, but append generator-owned ?v=... cache-busters through helpers/asset-url.html; keep fingerprinted asset links for non-development builds
  • for inline module bootstraps that need shared asset URLs, prefer an adjacent application/json config node over embedding quoted import specifiers directly in JS source; this avoids broken import("\"/cdn/...\"") output in templated module scripts like PhotoSwipe and Mermaid
  • prefer tiny site-specific head/import overrides over full site head.html forks whenever possible
  • do not create per-site layouts/ theme forks in individual website repos; shared rendering changes belong in generator/layouts/
  • do not create per-site theme copies or symlinks like themes/quantalumin/generator in site repos; run Hugo against the shared theme instead, for example with --themesDir /home/henry/Code
  • the SPA prefetch runtime rebases relative src / href / poster / data / srcset URLs inside fetched main/aside content before swapping DOM, so bundle-local media like petru.jpg keep working after client-side navigation
  • when debugging, verify the rendered page head includes the canonical runtime chain:
    • prefetch/prefetch.js
    • jquery/jquery.min.js
    • vim/vim.js
    • custom/codeCopyBtn.js
    • websocket/cursor.js
    • editor/nowtype.min.js
    • custom/nowtypePdfCore.js
    • custom/toggleMarkdown.js

Styling Migration Note

The current styling migration direction is documented in docs/scoped-surface-css-plan.md.

Short version:

  • keep one shared variable vocabulary
  • scope the same names at header, main, footer, aside, and section level
  • treat banner, spotlight, and wrapper as legacy presets over a smaller shared section model
  • migrate from SCSS to CSS incrementally, not in a single rewrite
  • keep the shared SCSS breakpoint map scalar; tuple/range values can compile into invalid media queries and silently skip responsive rules
  • the shared main.scss build now goes through Dart Sass in layouts/partials/head/css.html rather than deprecated libsass, so modern CSS math and shared component styles compile reliably during development and production builds

The shared breakpoint contract lives in:

  • assets/scss/main.scss
  • assets/scss/libs/_breakpoints.scss

Tuple breakpoints are interpreted as (max, min) ranges. The mixin now emits valid <=, >=, and exact-range media queries from those tuples, so mobile section, gallery, and hero rules actually apply at the intended breakpoints.

Page Title Header

When the shared title contract decides a page should render the automatic title block, layouts/partials/main.html should emit that block as a semantic page-level <header class="content ql-page-header"> inside the title section, not as a plain .content div. That preserves the shared layout while keeping the authored page structure meaningful for tooling and downstream consumers.

Skill Meter Widget

The skill-meter shortcode now renders through a shared partial and shared SCSS/JS instead of injecting inline <style> or a nested full <section> blob into markdown output.

Files:

  • layouts/shortcodes/skill-meter.html
  • layouts/partials/widgets/skill-meter.html
  • assets/scss/components/_skill-meter.scss
  • cdn/custom/skillMeter.js

Supported shortcode params:

  • widget
  • user
  • variant
  • title
  • eyebrow
  • subtext
  • class

Use variant="compact" when the meter should behave like a secondary landing-page card rather than the dominant block on the page.

Quiz Widget

The quiz shortcode now provides a dedicated Hugo widget for authored quiz definitions instead of forcing quiz content into generic prose editing.

Files:

  • layouts/shortcodes/quiz.html
  • layouts/shortcodes/quiz.tex
  • layouts/partials/widgets/quiz-load.html
  • layouts/partials/widgets/quiz.html
  • assets/scss/components/_quiz.scss
  • cdn/custom/quiz.js
  • docs/quiz-widget.md

Recommended shortcode usage:

Quiz source "quizzes/cell-basics.json" was not found as a page resource or readable file.

The shortcode resolves src against page resources first, then falls back to readable files. Inline YAML/JSON is also supported when src is omitted.

In edit mode, file-backed quiz widgets now open through the dedicated Quiz Editor modal from the rendered page or when their JSON/YAML source file is opened directly. Inline shortcode quiz bodies remain raw-source only for now.

Intro Card Color Tokens

Intro-card sections now use shared surface-scoped foreground tokens for the card copy, rather than relying on whatever the surrounding section text color happens to be.

Available surface tokens:

  • intro_card_fg
  • intro_card_title_fg
  • intro_card_meta_fg
  • intro_card_lead_fg

These can be authored at site, page-type, or page scope through the normal params.style.tokens.surface.* / params.style.page_types.<scope>.tokens.surface.* / page style.tokens.surface.* paths.

Markdown section attrs can also carry inline CSS custom properties directly on the section root, for example:

{.banner .intro-card style="--surface-intro-card-fg:#ffdd9a;--surface-intro-card-title-fg:#ffdd9a;"}

The shared section renderer now preserves that style="" attribute alongside authored section classes, which is useful for one-off token tuning without introducing site-local layout forks.

Starfield Section Effect

Sites that want the shared Three.js starfield effect should opt into the runtime through params.cdnAssets, for example:

[params]
  cdnAssets = ['/custom/starfield.js']

Then apply the effect as a normal section modifier:

{.banner .intro-card .halfscreen .content-align-center .fx-starfield}

Optional fly-in text can stay on the section itself via CSS custom properties in the same attr line, for example --ql-starfield-text-1, --ql-starfield-text-2, and --ql-starfield-text-3. By default the fly-in text uses the site body font; set --ql-starfield-text-font only when a section needs a different family. The moving stars default to small white points; only set --ql-starfield-node, --ql-starfield-node-accent, --ql-starfield-node-size, or --ql-starfield-node-opacity when a section explicitly wants a more pronounced secondary node layer. That keeps the effect on the shared generator path without reviving old front-matter-driven custom partials.

Team Block

Use a fenced team block or team shortcode for committee/staff/profile cards instead of raw HTML grids. The widget renders the existing brochure card layout (.ql-team-grid / .ql-team-card) from structured data, so the markdown stays readable and the editor has a clearer object boundary to target later.

Example fenced block:

```team
- name: Petru Balan
  role: Hall Manager
  image: petru.jpg
  alt: Portrait of Petru Balan.
- name: Helen Mullany
  role: Chair
```

Supported fields per member:

  • name
  • role
  • image / src / photo
  • alt
  • bio
  • href / url / link
  • link_label
  • class

Text-only cards are allowed by omitting image.

In live edit mode, rendered team blocks are now treated as a structured object instead of a raw HTML blob. Clicking a team grid opens a Team Editor modal that edits just that fenced block in the page markdown and currently supports:

  • add/remove members
  • move members up/down
  • edit name, role, image, alt, bio, href, link_label, class, loading
  • upload/replace member photos through the existing project image upload path

The current editor writes back to the fenced team block in the source markdown file. It does not yet provide direct on-canvas drag/reorder handles.

Workspace Block

Use a fenced workspace block or workspace shortcode for shared collaboration spaces that mix real people and agents on the same website-backed board. This keeps the collaboration surface file-backed in markdown while giving the editor a clear object boundary to target.

Example fenced block:

```workspace
title: Generator Collaboration Room
summary: |
  Shared workspace for people and agents collaborating through the website.
website: /workspace/
repo: https://forgejo.example.com/org/repo
chat: https://control.example.com/rooms/generator

participants:
  - name: CTO
    type: agent
    role: Technical lead
    company: quantalumin
    department: leadership
    summary: |
      Coordinates architecture and delivery.
  - name: Alice Example
    type: person
    role: Client representative
    company: Example Studio
    summary: |
      Reviews copy, goals, and launch priorities.
```

Supported top-level fields:

  • title
  • summary
  • objective
  • website / site
  • repo / repository
  • chat / room
  • notes / docs
  • class
  • participants / members / people / collaborators

Supported fields per participant:

  • name / title
  • type / kind
  • role / position
  • company / org
  • department / team
  • status
  • image / src / photo
  • alt
  • summary / bio / body
  • href / url / link
  • link_label
  • website / site
  • workspace / board
  • session / thread
  • class
  • loading

In live edit mode, rendered workspace blocks are now treated as a structured object. Clicking a workspace board opens a Workspace Editor modal that edits just that fenced block in the source markdown and currently supports:

  • top-level workspace settings like title, summary, objective, website, repo, room, and notes
  • add/remove participants
  • move participants up/down
  • edit participant type, role, company, department, status, links, session, and summary
  • upload/replace participant photos through the existing project image upload path

Use a fenced gallery block or gallery shortcode for brochure/gallery image grids instead of raw HTML. The widget renders the existing brochure gallery layout (.ql-gallery-grid / .ql-gallery-item) from structured data, so the markdown stays readable and the editor can treat the block as one object boundary.

Example fenced block:

```gallery
- image: rawson-large-hall-full.jpg
  alt: The large hall arranged for an event.
  caption: The large hall set up for a formal gathering.
- image: rawson-field-from-large-hall.jpg
  alt: The view from the hall towards the field outside.
  caption: Views out from the hall across the green space.
```

Supported fields per item:

  • image / src / photo
  • alt
  • caption / text / body
  • class
  • loading

In live edit mode, rendered gallery blocks are treated as a structured object. Clicking a gallery grid opens a Gallery Editor modal that edits just that fenced block in the page markdown and currently supports:

  • add/remove images
  • move images up/down
  • edit image, alt, caption, class, loading
  • upload/replace gallery images through the existing project image upload path

On pages with pswpFigures: true, the shared PhotoSwipe boot still upgrades rendered figures inside .ql-gallery-grid at runtime, so the gallery block keeps the old click-to-expand behavior without needing raw HTML.

Stats Block

Use a fenced stats block or stats shortcode for metric and highlight cards instead of raw HTML. The widget renders the brochure stat layout (.ql-stat-grid / .ql-stat-card) from structured data, so the markdown stays readable and Hugo remains the render authority.

Example fenced block:

```stats {.ql-grid-4}
- value: Main Hall
  label: Large event space
  body: 22m x 9m with space for larger classes, parties and events.
  class: featured
- value: From GBP 13/hr
  label: Starting price
  body: Room hire starts with Robins' Room and scales up by space.
```

Supported fields per item:

  • value
  • label
  • body
  • class

The block preserves source order in the rendered DOM and keeps the stat grid wrapped in a single data-ql-editable="stats" outer element.

FAQ Block

Use a fenced faq block or faq shortcode for question-and-answer lists instead of raw HTML. The widget renders a structured FAQ layout (.ql-faq-grid / .ql-faq-item / .ql-faq-answer) from markdown data, so FAQ content stays readable while Hugo remains the render authority. FAQ items now reuse the shared details.box.collapse hover/open treatment, so sites get the same accent-corner hover behavior as collapse boxes without custom site CSS. FAQ summaries now use a full-row click target, with the +/ glyph treated as a non-interactive indicator, and the shared section rhythm also gives faq blocks the same automatic breathing room before following button rows as other structured blocks.

Example fenced block:

```faq
- question: Can I book recurring sessions?
  answer: Yes. Recurring hires are supported where availability allows.
- question: Is the hall accessible?
  body: |
    The main hall has step-free access and accessible toilet facilities.
  class: highlighted
```

Supported fields per item:

  • question
  • answer / body
  • class

The block preserves source order in the rendered DOM and keeps the FAQ grid wrapped in a single data-ql-editable="faq" outer element.

Contact Block

Use a fenced contact block or contact shortcode for profile/contact-card grids instead of raw HTML. The widget renders the existing brochure contact layout (.ql-contact-grid / .ql-contact-card / .ql-contact-profile) from structured data, so contact pages stay readable in markdown and the editor can treat the block as one structured object.

Example fenced block:

```contact
- type: profile
  name: Petru Balan
  role: Hall Manager
  image: petru.jpg
  alt: Portrait of Petru Balan.
  body: |
    Petru handles day-to-day hall enquiries and supports regular users, party organisers and
    prospective hirers.
- title: Contact details
  body: |
    **Telephone**<br>01444 300158

    **Email**<br>[info@example.com](mailto:info@example.com)
```

Supported fields per item:

  • type / kind
  • title
  • name
  • role
  • image / src / photo
  • alt
  • body / bio / text / content
  • href / url / link
  • link_label
  • class
  • loading

Items with type: profile or an image render as .ql-contact-profile cards; items without an image render as ordinary .ql-contact-card panels.

In live edit mode, rendered contact blocks are treated as a structured object. Clicking a contact grid opens a Contact Editor modal that edits just that fenced block in the page markdown and currently supports:

  • add/remove cards
  • move cards up/down
  • edit type, title, name, role, image, alt, body, href, link_label, class, loading
  • upload/replace contact profile images through the existing project image upload path

Testimonials Block

Use a fenced testimonials block or testimonials shortcode for quotation/testimonial grids instead of raw HTML. The widget renders brochure quote cards (.ql-testimonial-grid / .ql-card.ql-quote-card) from structured data, so homepage/social-proof sections stay readable in markdown and the editor can treat the whole grid as one structured object.

Example fenced block:

```testimonials
- quote: The hall worked beautifully for our family celebration.
  name: A Rawson hirer
  role: Birthday party
- quote: Clear booking process and a really flexible local venue.
  name: Community organiser
  role: Weekly class
```

Supported fields per item:

  • quote
  • name
  • role
  • class

In live edit mode, rendered testimonials blocks are treated as a structured object. Clicking a testimonials grid opens a Testimonials Editor modal that edits just that fenced block in the page markdown and currently supports:

  • add/remove items
  • move items up/down
  • edit quote, name, role, class

Booking Block

Use a fenced booking block or booking shortcode for public reservation widgets backed by the shared booking API instead of raw HTML forms or site-local embeds. The block renders a shared .ql-booking-widget shell, fetches availability from the booking service, and redirects users into the hosted payment checkout returned by the API. The public widget now also supports calendar-style day, week, month, and year views so visitors can see what is on as well as what is available to book. In edit mode, clicking the rendered widget opens Booking Editor, which lets authors update the widget settings and room/resource list without dropping back to raw HTML. The selected day agenda also renders public event cards with safe source/category/room metadata from the booking API, so the same widget can act as a what’s-on surface as well as a reservation flow. When a block omits provider or common booking URLs, the shared partial falls back to site.Params.booking.* so sites can set shared booking defaults in Hugo config instead of duplicating them in page content.

Example fenced block:

```booking
title: Book Rawson Hall
intro: Choose a room, pick a slot, and continue to secure payment.
view: month
charges_url: /charges
terms_url: /terms-and-conditions
privacy_url: /privacy-policy
resources:
  - owner: rawson-main-hall
    name: Main Hall
    description: Large hall for parties, classes, and community events.
    price_lookup_key: rawson.main-hall.standard
  - owner: rawson-small-hall
    name: Small Hall
    description: Flexible room for meetings and smaller groups.
    price_lookup_key: rawson.small-hall.standard
```

Supported top-level keys:

  • title
  • intro / description
  • provider
  • view (day, week, month, year, or legacy list; default month)
  • api_base_url
  • success_url
  • cancel_url
  • charges_url
  • terms_url
  • privacy_url
  • items / resources / rooms

Supported fields per resource item:

  • owner
  • name / title
  • description
  • price_lookup_key
  • class

For brochure sites served through sites-edge, prefer a same-origin shared base such as /_platform/booking. The edge can then proxy and cache the public booking API without exposing a cross-origin dependency from the page runtime.

Calendar Block

Use a fenced calendar block or calendar shortcode for a separate public what’s-on calendar surface backed by the shared booking API. This block uses the same shared planner UI as the booking widget, but it renders an event-focused agenda instead of a reservation form. It is the preferred shared surface for whole-site or cross-room public schedules.

The shared booking/calendar runtime in bookingWidget.js must re-scan widgets after SPA swaps. The current contract is to listen for the shared spa:route event from prefetch.js instead of assuming a full page load.

Example fenced block:

```calendar
title: What’s on at Rawson Hall
intro: See public events across the hall, then jump into the booking flow when you want to reserve a room.
view: month
booking_url: /bookings/
booking_label: Book a room
owners:
  - owner: rawson-main-hall
    name: Main Hall
    description: Parties, classes, and community events.
  - owner: rawson-small-hall
    name: Small Hall
    description: Meetings, workshops, and smaller gatherings.
```

Supported top-level keys:

  • title
  • intro / description
  • view (day, week, month, year, or legacy list; default month)
  • api_base_url
  • booking_url
  • booking_label
  • owners / items / resources / rooms

Supported fields per owner/filter item:

  • owner
  • name / title
  • description
  • class

Platform Feed Block

Use a fenced platformfeed block or platformfeed shortcode for public same-origin widgets backed by sites-edge path routes like /_platform/villagehall. This is the preferred pattern for brochure-first sites that need a live news feed, bulletin board, or event list without coupling the static site generator to the portal backend.

The shared runtime in platformFeedWidget.js listens for spa:route and ql:page-swapped, so these widgets continue to hydrate on client-side navigation.

Example fenced block:

```platformfeed
title: Latest village news
intro: Notices and updates from the hall and the wider village.
kind: feed
api_base_url: /_platform/villagehall
site_slug: rawson-hall
category: village_hall
limit: 4
cta_url: https://portal.rawsonhallbolney.org/
cta_label: Open the community portal
```

For public events:

```platformfeed
title: Upcoming events
kind: events
api_base_url: /_platform/villagehall
site_slug: rawson-hall
timeline: upcoming
limit: 6
cta_url: /events/
cta_label: View all events
```

For public documents:

```platformfeed
title: Hall documents
kind: documents
api_base_url: /_platform/villagehall
site_slug: rawson-hall
limit: 6
cta_url: /hall-documents/
cta_label: Browse hall documents
```

Supported top-level keys:

  • title
  • intro / description
  • kind (feed, events, or documents)
  • api_base_url
  • endpoint
  • site_slug
  • category
  • timeline
  • limit / item_limit
  • cta_url
  • cta_label
  • empty_text
  • class

Platform Portal Block

Use a fenced platformportal block or platformportal shortcode for a simple brochure-site CTA that sends residents into the full VillageHall portal.

Example fenced block:

```platformportal
title: Residents’ portal
intro: Sign in to join the noticeboard, post updates, and manage member-only activity.
url: https://portal.rawsonhallbolney.org/
label: Open the portal
```

Supported top-level keys:

  • title
  • intro / description
  • url / portal_url
  • label / cta_label
  • class

Fundalumin Block

Use a fenced fundalumin block or fundalumin shortcode for shared funding discovery widgets backed by the separate Fundalumin app. The block renders a scoped .ql-fundalumin-widget shell, aliases the surrounding site tokens into local --widget-* variables, resets shared component leakage inside the widget root, and then rebuilds the widget controls explicitly. That keeps the widget on the shared generator path while still letting each site inherit its own branding via Hugo-configured CSS variables. The shared runtime in fundaluminWidget.js also re-scans widgets after shared SPA swaps by listening for spa:route and ql:page-swapped, so discovery pages keep working on client-side navigation instead of only on hard reloads.

Example fenced block:

```fundalumin
title: Fund a Future Luminary
intro: Support TutorLumin students and help turn donations into tuition credit.
app_base_url: http://127.0.0.1:8090
browse_url: http://127.0.0.1:8090/
browse_label: Open Fundalumin
apply_url: /enrol/
apply_label: Apply for support
wishlist_limit: 3
campaigns:
  - slug: general-fund
    label: TutorLumin Access Fund
    description: Support tuition credit for students who need it most.
    cta_label: Support this fund
  - slug: mina-medicine
    label: Mina's medicine goal
    description: Help Mina continue her science studies with tuition support.
```

Supported top-level keys:

  • title
  • intro / description
  • app_base_url
  • browse_url
  • browse_label
  • apply_url
  • apply_label
  • wishlist_limit
  • campaigns / items / profiles

Supported fields per campaign item:

  • slug
  • label
  • description
  • url
  • cta_label
  • class

The first editor-side token inspector now reads from that canonical surface:

  • Site scope reads Hugo config-backed defaults via params.style.tokens.*
  • Page Type scope reads shared surface defaults via params.style.page_types.<scope>.tokens.surface.*
  • Page scope reads front matter overrides via style.tokens.surface.*
  • Section scope reads and writes the selected live section against the authored section ordinal (s0, s1, …) and owns the leading section attr block in markdown
  • Block scope currently targets section media attrs first (s0:media) and structured fenced blocks by kind + ordinal (team:0, gallery:1, contact:0, stats:0, testimonials:0, faq:0, booking:0, calendar:0, fundalumin:0, buttons:0), then falls back to explicit attr/fence-backed block ids when they exist
  • the current panel still keeps legacy section / region groups as compatibility only, while surface is the primary shared editing surface

Structured fenced blocks now also share one editor-side descriptor registry in cdn/custom/toggleMarkdown.js for slash/backslash insert templates, editor labels, model keys, parse/replace handlers, inspector renderers, modal routing, and preview targeting. New blocks should extend that shared registry instead of adding more one-off switch branches.

Within that registry, the simpler YAML-list blocks faq, testimonials, and stats now also share one list-editor engine for item add/move/remove/update and inspector rendering. The richer media/profile blocks team, gallery, and contact now share the same card-list scaffolding and list mutation helpers, while still keeping block-specific image/upload/autofill widgets on top.

In replace-main mode, that token editor now lives inside the first shared right-side inspector shell. The shell exposes stable Content, Layout, Style, Data, and Advanced tabs for the current live selection or page defaults. Style is still the strongest shared panel in this slice, but Layout now also edits the existing section contract directly for normal section controls instead of only launching the old popover.

Focused Nowtype section controls now reuse that same shared inspector too: the section Layout button opens the shared inspector with an explicit section context, so Layout and Style still work even when there is no live-page selection object.

The old floating Nowtype section-layout popover has now been removed. Section layout editing should only route through the shared inspector, so the editor no longer carries a second layout-only UI path beside the semantic inspector.

The next live section inspector slice is now in place in toggleMarkdown.js. For a selected live section it now exposes the new semantic axes first:

  • flow
  • media_side
  • size
  • content_align
  • surface
  • layout_family as a compatibility preset

It then keeps width, spacing, padding, density, and the existing block layout preset path in a separate Width And Rhythm card.

The first layout mutation still opens the focused replace-main section editor so the markdown buffer stays authoritative, then applies the shared control from that same inspector rail. The write path is now split more cleanly:

  • section-frame semantics like family, media side, size, spacing, padding, density, surface, and content alignment write to the section body’s leading { ... } attr block
  • content width and block layout presets stay on the section body’s trailing --- { ... } content attr block

frontmatter.sections[*].section remains editor compatibility only; the active Hugo render path now follows the inline markdown attr blocks above as the canonical section-frame source.

Shared Navigation Ordering

Page ordering for navigation partials is centralized in layouts/partials/nav/ordered-pages.html.

It is used by nav/nextprev, nav/book-index, and book TOC partials, and sorts by:

  • explicit front matter weight (when set),
  • numeric filename/dirname prefixes like 1-, 2-, 10-,
  • stable path/title/permalink tie-breakers.

Inline markdown/render-hook figures may link into PhotoSwipe, but promoted section-media figures are intentionally non-PhotoSwipe.

Pages can opt plain gallery figures into PhotoSwipe by setting front matter like pswpFigures: true (or pswpGallery: true). On those pages, the shared PhotoSwipe boot upgrades figures inside .ql-gallery-grid into pswp-item links at runtime before initializing the lightbox, so the visible page markup can stay unchanged.

Fixed Chrome Shell

Sites can opt into a fixed header/footer shell via config:

[params.chrome]
  fixedFrame = true
  showArticleNav = false

When fixedFrame is enabled, the shared generator keeps <header> and <footer> always visible, makes <main> the scroll container, and adds the body class ql-shell-fixed-chrome. showArticleNav = false suppresses the bottom #article-nav chrome without requiring a site-local layout fork.

The shared background audio player can also auto-close its initial minimised bar after page load:

[params.bgfx]
  player = true
  autoCloseBarAfterMs = 3500

This only affects the initial idle bar state. It does not overwrite the saved player mode in local storage, and the auto-close timer is cancelled on first user interaction.

Markdown render-hook figure numbering now uses a page-global counter in layouts/partials/fig/parse.html (.Page.Store) so captions do not reset when page content is rendered in multiple chunks.

Section-media extraction is now opt-in: only images marked with {.fig}, {.figure}, or {.section-banner} are treated as section figures. Plain markdown images stay inline in .content.

{#fig:...} shorthand is normalized to id="fig:..." during preprocessing, including when the attribute block is written on the next line after the image or below caption lines.

Section-media figures are implicitly non-linking. .nolink / .no-link is still tolerated on read for backward compatibility, but it is redundant and should not be written for new section-media source.

Plain markdown image layout now resolves in this order:

  • explicit image layout from attrs/classes such as .fit, .left, .right, .centre, .main
  • explicit destination suffix such as ![...](image.jpg#fit)
  • otherwise a shared fallback:
    • fit on homepage/list/taxonomy pages
    • alternating left / right on ordinary single pages

The shared image render hook must tolerate pages rendered without a backing .Page.File object, such as RSS and some indirect/module render contexts. When deriving figure/gallery ids, use .Page.File.Path only when present and fall back to .Page.Path or .RelPermalink.

iframe-like markdown image targets (.html, .pdf, YouTube, and video files) default to fit unless you explicitly choose another layout, so ![](widget.html) behaves like an embed instead of a floated prose image. Local and external .html targets now both use the shared iframe render path. External URLs that expose an embed endpoint under /embed also use that iframe path, so markdown like ![](https://www.instagram.com/example/embed) stays on the plain ![]() authoring flow instead of requiring raw HTML in content.

Homepage/list lead sections should be authored as ordinary first sections. If the first section is .banner or .spotlight, it owns the visible page title while front matter title stays available for SEO, nav, and feeds. The shared intro-card look is now a real section modifier, so authored section attrs augment site defaults like fullscreen instead of replacing them. Shared title ownership also now treats leading markdown section attr lines ({.wrapper ...}) as neutral, so pages that begin with attrs followed by # Heading keep the content heading as the visible page title instead of getting a duplicate template title. Shared content-only fullscreen banners and spotlights now size against the viewport minus a small chrome offset (--fullscreen-content-offset) rather than a raw 100vh, so the hero sits higher and the footer can remain visible on initial load.

Section/figure class ownership is now classifier-based. Figure attrs may still promote a small legacy subset onto the section root when they clearly express section semantics:

  • layout families: banner, spotlight, wrapper
  • section modifiers: styleN, fullscreen / halfscreen, content-align-*, and shared fade classes

Figure-only media tokens such as fit, left, right, main, and other media-layout controls stay on the <figure>. This keeps legacy figure-led sections like ![](floating-molecules.svg) {.fig .fit .spotlight .style4 .fullscreen} working as section-level spotlight style4 fullscreen blocks without leaking fit onto the section. footer can still enter view on shorter laptop screens without site-specific overrides.

{.banner .intro-card}

<div class="kicker">Bolney village venue</div>

# Classes, parties, weddings and community events in the heart of Bolney

Hire bright, flexible spaces for fitness classes, children's parties, meetings, celebrations and village events.
{.lead}

For split layouts, use the same pattern with .spotlight:

---
{.spotlight .intro-card}

# Section title

DJ Chris Nova • Canterbury, UK • Quantum Physicist

One strong paragraph introducing the section.
{.lead}

The old .ql-hero, .ql-hero-card, .ql-kicker, .ql-hero-meta, and .ql-ink classes still work as temporary compatibility aliases, but the shared SCSS section styles now own the look. Keep new content on section modifiers and ordinary content classes rather than page-level hero wrappers.

The legacy fenced hero block is no longer the canonical authoring path.

Section-media figures render as direct section media with a layout class:

  • explicit .left / .right / .fit in the media attributes wins,
  • otherwise layout alternates left/right by figure order (same default behavior as markdown render-image parsing).

Wrapper SVG Margins

Wrappers can render decorative SVG side margins using section classes:

  • svg-margin or svg-margin-lines: vertical line motif on both sides,
  • svg-margin-motif: repeating square motif on both sides.

Example section class:

section: "wrapper style1 content-align-center svg-margin-motif"

Bundle-local SVG from front matter:

content/1-background/
  _index.md
  border.svg
sectionStyle: "svg-margin"
sectionMarginSvg: "border.svg"

sectionMarginSvg resolves against page resources first (so border.svg in the same bundle works), then falls back to a direct path/URL.

Preset + side controls:

sectionStyle: "svg-margin"
sectionMarginPreset: "french-flag" # tricolor stripe preset
sectionMarginSide: "left"          # left | right | both (default both)

Current presets:

  • french-flag
  • orbitals-spdf (stacked s/p/d/f motif in blue/red/purple/green)
  • power-grid (animated green sci-fi wiring grid)

Preset framework:

  • sectionMarginPreset: "<slug>" maps to CSS class .svg-margin-preset-<slug>,
  • add new preset styles in assets/scss/components/_wrapper.scss under .wrapper.

Inheritance (unless overridden):

  • child pages inherit sectionStyle and sectionMargin* from their current section (_index.md),
  • child front matter values override inherited ones,
  • set sectionStyle: "none" on a child page to disable section wrapper styling.

You can tune spacing/scale/opacity per section by setting CSS vars on that wrapper:

  • --ql-svg-margin-inset
  • --ql-svg-margin-strip-width
  • --ql-svg-margin-tile-height
  • --ql-svg-margin-opacity
  • --ql-svg-margin-svg (full custom SVG data URL)

Equivalent front matter vars are also supported:

  • sectionMarginInset
  • sectionMarginStripWidth
  • sectionMarginTileHeight
  • sectionMarginOpacity

Content Iframes

Banner and spotlight layouts no longer blanket-scale every descendant <iframe>. Only figure-owned/media iframes keep the old scaled embed treatment. Raw content iframes inside .content now retain their authored width and height, so embeds like booking diaries and maps can fill the text column instead of being silently shrunk to half size.

Embed Shortcode

For richer iframe/embed layout control, use the figure-backed embed shortcode instead of raw HTML:

Rawson Hall booking diary

Supported parameters:

  • src or url: required embed URL/path
  • title: iframe title / figure title
  • caption: optional figcaption text (or use shortcode body)
  • layout: figure layout such as fit, left, right
  • class: extra figure classes
  • width: optional figure width passed through the normal figure width parser
  • height: fixed iframe height when not using an aspect ratio
  • aspect: aspect ratio such as 16 / 9 or 4 / 3
  • allow, allowfullscreen, sandbox, referrerpolicy, loading, iframeId

If the shortcode src is a YouTube URL, local/external .html, .pdf, or a supported video file, it still routes through the normal media pipeline. Otherwise it defaults to a real iframe embed rendered inside the standard figure wrapper, so captions and layout classes behave like other media.

Scoped Surface Widths

The theme now has a first shared width contract for page surfaces:

  • main uses --main-content-max-width
  • header uses --header-content-max-width
  • footer uses --footer-content-max-width
  • aside uses --aside-content-max-width

These default to the prose width token surface and expose a shared --surface-content-max-width inside each region.

body.ql-page-type-homepage, body.ql-page-type-list, body.ql-page-type-single, body.ql-page-type-landing, and body.ql-page-type-detail now rebind main through the same shared surface contract.

Current shared page scopes:

  • homepage
  • list
  • single compatibility umbrella for all regular pages
  • landing first-level singleton pages such as /about/, /contact/, /bookings/
  • detail deeper regular pages such as /museum/chromatography/

main now exposes the primary page scope as data-ql-page-type, while <body> keeps the compatibility ql-page-type-single class alongside ql-page-type-landing or ql-page-type-detail where relevant.

The scoped main bridge now drives:

  • --main-content-max-width
  • --surface-content-max-width
  • --surface-section-padding-y
  • --surface-section-gap-y
  • --lead-max-width

That lets top-level brochure-like pages stay wider with roomier lead spacing, while deeper detail pages can fall back to a tighter prose rhythm without per-site overrides.

Shared Section Spacing

Section spacing is now moving onto a shared rhythm contract instead of relying on hardcoded utility exceptions.

Shared scoped spacing tokens now include:

  • --section-gap-before
  • --section-gap-after
  • --section-padding-top
  • --section-padding-bottom
  • --section-gap-tight
  • --section-gap-normal
  • --section-gap-roomy
  • --section-padding-y-tight
  • --section-padding-y-normal
  • --section-padding-y-roomy

main rebinds those through page scopes such as homepage, list, landing, and detail, so ordinary section rhythm can be driven automatically by shared CSS. The first rendered section inside main is then reset to --section-gap-before: 0, so section spacing remains section-to-section rhythm rather than creating a blank band between header and the first section.

Shared density defaults now also flow through main by page scope:

  • --surface-structured-gap
  • --surface-compact-padding-y
  • --surface-intro-card-gap
  • --surface-intro-card-padding-y
  • --surface-intro-card-padding-x
  • --surface-intro-card-max-width
  • --surface-intro-card-mobile-max-width
  • --surface-component-gap
  • --surface-card-padding-y
  • --surface-card-padding-x
  • --surface-note-padding-y
  • --surface-note-padding-x
  • --surface-chip-gap
  • --surface-chip-padding-y
  • --surface-chip-padding-x
  • --surface-team-card-gap
  • --surface-team-card-padding-y
  • --surface-team-card-padding-x
  • --surface-action-gap
  • --surface-embed-fixed-max-height-medium
  • --surface-embed-fixed-max-height-small
  • --surface-body-line-height
  • --surface-heading-line-height
  • --surface-paragraph-gap
  • --surface-heading-gap
  • --surface-nav-row-gap
  • --surface-nav-column-gap
  • --surface-nav-link-size
  • --surface-nav-link-line-height
  • --surface-nav-link-padding-x
  • --surface-nav-link-padding-y

That lets landing pages stay more brochure-like while detail pages tighten rhythm and intro-card surface sizing automatically, without page-specific CSS. Brochure-style components such as grids, cards, notes, fact chips, action rows, team cards, fixed-height embeds, paragraph rhythm, heading rhythm, and header/menu density now consume the same scoped density surface.

Global params.style.tokens.surface.nav_link_size feeds the page-type nav defaults, so the shared header size can be changed once at framework level or overridden once per site without repeating the same override for homepage, list, landing, and detail.

Header/menu sizing now binds through the header surface itself, not the main page-type surface. That keeps ordinary header links and button-like CTA items on the same shared nav token path.

Headers now also have a first shared contract layer in params.header.*, with legacy params.style.theme.header = "style1|style2|style3" still supported as compatibility presets. The new contract is:

  • layout = inline | split | stacked | centered
  • logo_position = left | center | hidden | title
  • nav_align = start | center | end | space-between | space-evenly
  • density = compact | normal | roomy
  • surface = solid | glass | transparent
  • sticky = true | false
  • cta_mode = inline | separate | button

layouts/partials/page/header-contract.html maps those settings onto shared header classes and data attributes. Semantic header layout classes are now the primary runtime path, while style1 / style2 / style3, params.chrome.stickyHeader, and the legacy fixed header class continue to work only as compatibility inputs into the same contract.

The shared inspector now exposes the first semantic Header card in the Layout tab when no live section is selected. That card edits site and page header settings through the local bridge, using:

  • /ops/header/read
  • /ops/header/set
  • /ops/header/unset

Those bridge ops currently target params.header.* in the site config and page front matter header.*, preserving markdown/config-as-source rather than introducing a separate header editor store.

The rendered header shell now reads that contract once in layouts/partials/header.html and passes the semantic view model down to the logo and breadcrumb partials. Those partials prefer params.header.* first and only fall back to legacy HeaderNav.* fields when the semantic values are not present yet.

The local editor bridge now exposes that shared scoped model directly through a first-class surface token group in /ops/token/catalog. New editor token work should target:

  • global params.style.tokens.surface.*
  • local --surface-* section/block overrides in markdown attr styles

Legacy section / region token groups remain readable and writable for compatibility, but they are no longer the preferred editor path.

Compatibility aliases still work on wrapper, banner, and spotlight, but now only by rebinding the shared spacing vars:

  • .squeeze
  • .squeeze-top
  • .squeeze-bottom
  • .pad
  • .pad-top
  • .pad-bottom

New canonical spacing modifiers are:

  • .space-none
  • .space-tight
  • .space-normal
  • .space-roomy
  • .pad-none
  • .pad-tight
  • .pad-normal
  • .pad-roomy

Automatic adjacency tightening now lives in shared section SCSS for common structured-block transitions such as:

  • grid -> buttons
  • grid -> note
  • note -> grid
  • buttons -> note
  • heading/lead/kicker -> grid/note/buttons
  • figure-led media -> content

Automatic compacting now also handles common short sections such as:

  • a single note / facts block / buttons row
  • a heading plus one semantic module
  • short update sections like heading + paragraph or heading + note
  • short banner/content-only intro sections with exactly one heading and one paragraph

banner.style1 responsive padding now also respects the same shared --section-padding-* surface variables as wrapper.style1, so compact section heuristics and page/site token overrides work the same way across the two main content-only section presets.

That keeps the same spacing behavior for both normal Hugo output and the JS editor’s live page surface without duplicating spacing rules in JavaScript.

The intended rule is:

  • automatic rhythm from page scope + section flow + adjacency first
  • canonical .space-* / .pad-* only when a page genuinely needs an exception
  • legacy .squeeze* / .pad* aliases only as compatibility bridges

You can check that authored source has stopped relying on the legacy aliases with:

node scripts/check-legacy-spacing-usage.mjs /path/to/site /path/to/another-site

You can also verify that page-scope hooks are still emitted correctly with:

node scripts/check-page-scope-contract.mjs

For normal content sections, wrapper consumes that width contract instead of hardcoding everything to --size_inner. Content-only banner and spotlight sections now consume the same shared surface width by default, and still honor the same width classes when authored explicitly. Media-bearing banner and spotlight sections keep their split layout geometry. This keeps today’s presets compatible while the section model is generalized beyond the old banner/spotlight/wrapper split. Markdown section content can opt into a clearer width modifier by ending a section with an inline content attr block:

Some section body...
--- {.width-wide}

Current width modifiers:

  • .width-prose
  • .width-medium
  • .width-narrow
  • .width-wide
  • .width-full

This is the intended path for pages like booking diaries, maps, floorplans, and other content that should be wider than the normal prose column without forcing a different section family.

Live Edit Metadata

The public page HTML should keep the live edit contract minimal.

For ordinary section / content / media nodes, live DOM should carry only the identity and state markers needed for selection:

  • data-ql-section-id
  • data-ql-block-id
  • data-ql-editable
  • transient edit-session markers like data-ql-edit-attached and data-ql-live-selected

The richer editor node contract belongs in #editorData, not repeated across every live node:

  • ObjectId
  • NodeKind
  • Role
  • Capabilities
  • InteractionProfile
  • SchemaKeys
  • PersistenceAdapter
  • SourcePath

Structured special blocks like team and buttons should also stay lean in the public DOM and identify themselves through data-ql-editable, with richer meaning coming from the editor runtime and the source text rather than repeated public node-contract attributes.

Editorial Hero Backgrounds

You can apply abstract geometric hero backgrounds to wrappers:

  • hero-bauhaus: warm Bauhaus-inspired circles/bars/diagonal blocks
  • hero-editorial: modern editorial grid + accent geometry

Example:

section: "wrapper style1 hero-bauhaus content-align-center"

For block-level styling in the markdown editor (Attrs -> Region Preset), new presets include:

  • Hero (bauhaus)
  • Hero (editorial)
  • Bauhaus wrap left
  • Bauhaus wrap right

Full-Page Graphic Sections (Book Layout)

Use wrapper class page-graphic for chapter/part splash pages that should behave as full pages in print/PDF:

section: "wrapper style1 fullscreen page-graphic hero-bauhaus content-align-center"

Notes:

  • page-graphic enforces page-safe fragmentation (break-before/after: page) in print.
  • fullscreen keeps it immersive on screen; page-graphic handles print page sizing.
  • Combine with hero-bauhaus or hero-editorial for abstract geometric backgrounds.
  • Keep core text in .content; decorative geometry remains in pseudo-layers.

Installation

Make sure to clone the submodule Font-Awesome icons git submodule update --init --recursive

Configuration

Notice Blockquote Hooks

render-blockquote.* supports custom notice syntax:

  • > [!style] Title (normal notice)
  • > [!style]+ Title (collapsible, open by default)
  • > [!style]- Title (collapsible, closed by default)
  • > ![style] Title (legacy alias, still supported)
  • > ![style]+ Title (legacy alias)
  • > ![style]- Title (legacy alias)

Behavior:

  • title is on the marker line,
  • subsequent lines are rendered as content.

Example:

> [!warning]+ Read this first
> Back up your data before running the migration.
> This step cannot be undone.

Supported notice styles:

  • note
  • todo
  • info
  • tip
  • warning
  • caution
  • important

Icon override:

  • add a block attribute after the quote, e.g. {icon="skull-crossbones"},
  • add block classes via attributes (e.g. {.my-callout}) to style notice wrappers,
  • icon names resolve through your icon partial conventions: Custom first, then Font Awesome (solid/, regular/, brands/).

Example with custom alert type:

> [!action] Action
> Style of action buttons like Mermaid zoom or block code copy-to-clipboard.
{icon="skull-crossbones"}

Compatibility notes:

  • GitHub-style alerts (> [!NOTE]) still render.

Local Markdown Workspace

The site markdown overlay now supports:

  • file navigator,
  • in-browser markdown editing with Nowtype,
  • markdown round-trip for inline emphasis, links, and citations,
  • relative markdown src/href assets in Nowtype are resolved against the current page markdown path (so page-local figures render in edit mode),
  • bare relative image filenames in page bundles stay page-local during editor fallback repair (for example henry.jpg in about/index.md stays under /about/ and no longer degrades to /<name>),
  • the integration now passes the current markdown path into Nowtype via NT_SetFilePath(...) before NT_SetMarkdown(...), so runtime image preview candidates can resolve against the current file instead of guessing from the page root,
  • in replace-main edit mode, image figures get preview decoration (figure-inline + left/right layout + generated gallery id) and empty captions show a non-destructive preview label,
  • in edit mode, image previews are rendered as direct <img> nodes (no pswp-item/data-pswp-* link wrapping) so clicks can be used for editor figure actions,
  • figure captions render with italicized figure titles and use : before caption text when present; caption editing shows a Caption: helper label,
  • block-level raw HTML in markdown is now preserved as rendered, non-text Nowtype blocks instead of leaking literal source into the live editor surface, so authored structures like Rawson ql-facts / ql-grid-* blocks stay visually aligned with the webpage while text around them remains editable,
  • fenced structured blocks that Hugo already renders as real objects, such as team, gallery, stats, contact, and buttons blocks, are preserved in live in-place editing by reusing the original Hugo-rendered DOM as a non-text preview instead of leaking raw fence source into the visible page,
  • live in-place editing suppresses synthetic empty figure captions, so uncaptioned webpage figures no longer sprout preview-only labels like Figure 1,
  • content-only banner / spotlight sections whose content is just a direct fit figure or iframe embed now drop their inline content padding on narrow/mobile layouts, so shared embeds like Rawson /bookings/ can fill the section width instead of collapsing into a narrow inset card,
  • image Choose now opens a searchable picker modal of existing project image files (while still allowing typed path/URL apply),
  • Goldmark figure classes in {...} are applied onto Nowtype preview figures (including section markers like .section-banner),
  • figure placement remains figure-owned; canonical placement classes are position-* (for example .position-upper, .position-88), while legacy figure-position-* is still accepted on read for compatibility, and both forms are applied directly from the <figure> class list without needing section-level promotion,
  • content-only sections now strip figure-only section tokens such as orient-*, figure-position-*, and *-figure-fade-in during section class resolution, while home lead sections and explicitly authored fullscreen / halfscreen still keep their screen-size tokens,
  • same-line image Goldmark attrs like ![](hero.jpg) {.fig .no-link} are normalized to the two-line editor form before entering Nowtype, so section-media figures round-trip the same way as ![](hero.jpg) followed by {.fig .no-link},
  • preprocess now normalizes over-escaped typography entities inside heading tags after RenderString, so headings and body text do not diverge on smart apostrophes/quotes (for example Robins’ Room),
  • replace-main edit mode now consumes Hugo-emitted per-section #editorData for resolved section-media placement and layout (where, type, classes, layout, caption, sectionMedia) so banner/section figures are promoted using Hugo’s decisions rather than markdown-shape guesses alone,
  • replace-main loading/error states keep the top toolbar visible, and the same toolbar now remains available in live-page attach mode instead of disappearing after the initial bridge-backed page/file load,
  • bibliography preload now stat()-checks common ref.bib candidates before trying to read them, which avoids noisy missing-file bridge requests during editor startup,
  • callout round-trip + live callout rendering in editor for > [!type][+/-] Title,
  • Goldmark-style attributes for ids/classes/params ({#id .class key=value}),
  • hover Attrs popover in Nowtype for editing Goldmark id, classes, style, and extra params,
  • region presets now include optional wrapped layouts (region-wrap-left / region-wrap-right) so surrounding paragraphs can flow around selected note/figure regions,
  • editable replace-main page title ([data-page-title="true"]) bridged to front matter title,
  • PDF mode in Nowtype with a centered editable A4 viewport (A4 ratio + nt.print margins), page-step controls (prev/next), LaTeX book/article mode toggle, chapter title page in book mode, route-aware print actions (Chapter / Part / Book), scoped multi-file editing in PDF mode for part/book routes, and a dedicated print-preview document composed from the paginated PDF editor DOM rather than the browser’s generic flowing print layout,
  • polling-based external-change detection,
  • live page refresh via SPA navigation,
  • prefetched SPA HTML is revalidated against sitemap lastmod before reuse, and localhost navigation no longer reuses ready-cached page HTML after a rebuild, so stale route markup does not survive a Hugo rebuild and keep old image or iframe states alive until a manual reload.

Local disk mode (localhost)

Run the local bridge API in the theme repo:

node scripts/local-markdown-bridge.mjs --content-root /path/to/site/content --port 8787

Multi-site local test matrix:

  • TutorLumin Hugo:
    • hugo server --source "/home/henry/Design Resources (2)/Websites/Groups/tutorlumin.co.uk" --themesDir "/home/henry/Code" --bind 127.0.0.1 --port 1314 --buildDrafts --disableFastRender
  • TutorLumin bridge:
    • node scripts/local-markdown-bridge.mjs --content-root "/home/henry/Design Resources (2)/Websites/Groups/tutorlumin.co.uk" --port 8787
  • Rawson Hugo:
    • hugo server --source "/home/henry/Design Resources (2)/Websites/Groups/rawson" --bind 127.0.0.1 --port 1315 --buildDrafts --disableFastRender
  • Rawson bridge:
    • node scripts/local-markdown-bridge.mjs --content-root "/home/henry/Design Resources (2)/Websites/Groups/rawson" --port 8788

Expected local test endpoints:

  • TutorLumin site: http://127.0.0.1:1314/
  • TutorLumin bridge: http://127.0.0.1:8787/
  • Rawson site: http://127.0.0.1:1315/
  • Rawson bridge: http://127.0.0.1:8788/

Use this four-process setup as the default regression test harness when checking:

  • edit-mode startup and toolbar visibility,
  • live page editing/attach behavior,
  • bridge path resolution,
  • multi-site isolation between page roots and bridge roots.

When window.QLMarkdownEditor.localApiBase is omitted in localhost mode, the editor now infers the local bridge from the site port for the standard multi-site matrix:

  • 1313 -> 8787
  • 1314 -> 8787
  • 1315 -> 8788

That keeps Rawson and TutorLumin isolated during local regression runs even when the site override does not emit a site-specific localApiBase.

For source-backed local preview on a non-localhost host such as .qlocal, set these site params explicitly:

  • params.qlMarkdownEditor.defaultMode = "local" to force the browser onto the localhost bridge instead of WebDAV
  • params.qlMarkdownEditor.localApiBase = "http://localhost:8787" (or another explicit bridge URL)
  • params.qlMarkdownEditor.autoEnterEditMode = true to open the bridge surface on page load
  • params.qlMarkdownEditor.replaceMainInitialView = "focused" to start in the bridge-rendered replace-main view instead of the live-page attach mode

That combination keeps the page body sourced from the markdown bridge, so local markdown edits can appear immediately without waiting for another Hugo rebuild.

The local bridge now exposes GET /project/list (used by the image picker via kind=image).

Bridge contract status:

  • Implemented high-level ops:
    • set_local_token
    • unset_local_token
    • set_global_token
    • unset_global_token
    • read_token_catalog
    • read_global_tokens
    • read_local_tokens
    • set_attrs
    • clear_attrs
    • update_menu_item
    • create_page
    • insert_section
    • set_column_span
    • move_block
  • GET /capabilities returns the authoritative implemented op list in ops plus opEndpoints and coarse supports.* flags.
  • token read/catalog endpoints:
    • GET /ops/token/catalog
    • GET /ops/token/read-global
    • GET /ops/token/read-local?path=<markdown-path>&nodeId=<id>&scope=<section|block>&kind=<kind>
  • replace-main style inspector groundwork:
    • toggleMarkdown.js includes the first design-token inspector runtime and bridge wiring
    • replace-main now also exposes an initial shared right-side inspector shell with Content, Layout, Style, Data, and Advanced tabs
    • focused section Layout actions now route into that shared inspector instead of a separate popover, and the layout tab includes Density alongside family, width, spacing, padding, surface, and alignment
    • live selection now opens explicit section / block scopes instead of a single generic local bucket
    • the first panel slice now treats surface as the primary shared token group for width, spacing, density, typography, and nav
    • legacy section / region groups still render, but only as compatibility groups after surface
    • non-local scope targeting now distinguishes:
      • site
      • page type
      • page rather than a single global bucket
    • replace-main now also includes a first preview-width control on the existing toolbar (Desktop / Tablet / Phone) that constrains the live Nowtype paper width without introducing a second renderer
  • Attr/token mutation constraints:
    • POST /ops/token/set-local / unset-local accept target.scope:
      • section resolves the authored section by ordinal (s0, s1, …) and reads/writes its leading section attr block, creating it when needed
  • block currently resolves section media first (s0:media) and structured fenced blocks by kind + ordinal (team:0, gallery:0, contact:0, stats:0, testimonials:0, faq:0, booking:0, calendar:0, fundalumin:0, buttons:0), then explicit attr/fence-backed block ids
    • POST /ops/token/unset-global removes one scoped token assignment from:
      • params.style.tokens.<group>.<key> for site scope
      • params.style.page_types.<scope>.tokens.<group>.<key> for page-type scope
      • style.tokens.<group>.<key> in markdown front matter for page scope
    • POST /ops/attrs/set only mutates an existing Goldmark attr block found by target.path + target.nodeId
    • POST /ops/attrs/clear only clears id, classes, and CSS custom properties in an existing Goldmark attr block
    • attrs/set payload shape:
      • target: { path, nodeId }
      • payload.id: optional string, replaces #id
      • payload.classes: optional string array, replaces the full class list
      • payload.addClasses / payload.removeClasses: optional string arrays
      • payload.style: optional object of CSS custom properties to set
    • attrs/clear payload shape:
      • target: { path, nodeId }
      • payload.id: true removes #id
      • payload.classes: true removes all classes, or payload.classes: ["name"] removes only the listed classes
      • payload.style: true removes the full style="..." attr, or payload.style: ["--token"] removes only the listed CSS custom properties
  • Planned ops such as the remaining page/section/menu/site routes are documented in editor-plan.md but are not yet implemented.

The editor family is intentionally converging on three shared Nowtype-backed surfaces rather than separate rendering authorities:

  • page WYSIWYG editor
  • single-page PDF editor
  • double-page spread PDF editor

Those are editing surfaces over the same Hugo/markdown source contract, so shared token scopes and section semantics should work across all three rather than being re-implemented per mode. The prioritized product roadmap for making that feel more like a SquareSpace/Figma-class editor is in docs/editor-product-roadmap.md.

  • Current structural op limits:
    • set_column_span assumes explicit .ql-column wrappers
    • move_block assumes explicit .ql-block wrappers inside explicit .ql-column wrappers
  • Watch behavior:
    • GET /project/watch is available in local mode for project-root polling/SSE workflows
    • replace-main edit mode still falls back cleanly when watch is unavailable

The browser overlay will default to local mode on localhost. By default, edit mode uses editorPresentation: "replace-main": it hides page <main> and mounts the Nowtype workspace in its place. That replace-main surface uses embedded Nowtype (nowtype.min.js) with web-demo-compatible DOM anchors (#toolbar, #backbox, #paper, #render_div) and a minimal page-editor UI (current file context + editor area, no file-tree workbench, no iframe). inline and popup presentations remain optional configuration modes. The live-main attach path now uses Hugo-emitted data-ql-* identities and the formal section/content/media node contract (ObjectId, NodeKind, Role, Capabilities, InteractionProfile, SchemaKeys, PersistenceAdapter) to index the real rendered <main> while edit mode is active. In normal edit mode, clicking a live content block now opens true in-place Nowtype editing on that visible page content instead of falling back to a second full-page editor shell. Section media and section-chrome layout edits still route through the focused replace-main tools. The old live action panel (Text / Media / Layout / Tokens / Cancel) is no longer part of the normal workflow. Browser smoke now treats in-place content editing as the required contract on the local anchor pages (1314/about, 1315/, 1315/about-us/, 1315/facilities/). The facilities route also asserts that preserved raw HTML blocks stay rendered instead of leaking literal source text and that live in-place figures do not show synthetic empty captions. Live text sessions now preserve the original click point when placing the caret, and section structure/media controls are intentionally suppressed during an active live text session so stale section snapshots cannot clobber newer layout mutations. Remaining in-place caveats are deeper session rebasing if those controls are later re-enabled, empty-section UX polish, and richer editing for currently atomic raw-HTML blocks. PDF helper logic now has a dedicated runtime helper module at /cdn/custom/nowtypePdfCore.js, loaded before toggleMarkdown.js so PDF sizing/scope/page-role rules are centralized. PDF mode is available from the Nowtype toolbar and keeps the page centered while fixing page height to the viewport; LaTeX toggles book/article behavior, print scope follows the currently opened route (index.md => Book, top-level section _index.md => Part, deeper pages => Chapter), entering PDF mode on Part/Book routes loads an editable scoped document spanning all markdown files in that scope, an optional rendered ToC page sequence can be toggled from the toolbar and clicked to jump to headings, and figures that would cross a page boundary are shrunk/centered only at clean scale steps (0.95/0.9/0.85) or moved to the next page boundary. The print action now opens a separate print-ready document built from the same paginated PDF editor DOM (single-page or 2-up spread), so “Save to PDF” uses the editor’s composed page shells instead of reverting to the browser’s normal flowing print view.

For TeX/PDF output, SVG figures rendered through layouts/_default/_markup/render-image.tex now apply a doubled font context to the pdf_tex overlay layer, so LaTeX labels inside SVG figures render at roughly 2x their prior size.

For HTML output, inline SVG figures rendered through layouts/partials/figure/svg.html now scale the injected KaTeX <foreignObject> wrapper to roughly 2x as well, so $...$ / $$...$$ labels inside SVG figures stay visually consistent with the PDF path.

Browser Runtime Logging

For local debugging, the repo includes a small Playwright script that launches Firefox, opens your local site, and writes browser console/page/request failures to browser.log in the current working directory.

Default target:

  • http://127.0.0.1:1313

Start your site first, for example from the site repo:

cd ~/Projects/Research
./hugo dev --hugo-only

Then, from this repo:

npm run browser:log

Useful variants:

  • npm run browser:log:headed to watch Firefox
  • BROWSER_LOG_URL=http://127.0.0.1:1314 npm run browser:log for a non-default port
  • node scripts/browser-log.js http://127.0.0.1:1313 to pass the URL directly
  • npm run browser:smoke:pdf for a quick PDF-editor smoke (click-to-type focus, --- section page breaks, spread navigation)
  • BROWSER_SMOKE_URL=http://127.0.0.1:1313/2-methodology/ npm run browser:smoke:pdf to target a specific page
  • npm run browser:export:pdf -- --url http://127.0.0.1:1313/ --output /tmp/book.pdf to capture the current route through the editor’s print-ready PDF path
  • npm run browser:export:pdf -- --site-root /home/henry/Projects/Research/PhD --output /home/henry/Projects/Research/PhD/build/pdf/browser/phd-double-spread.pdf to auto-start the PhD site + local bridge and export the full book as a 2-up spread PDF

The export script drives the existing editor workflow rather than a second renderer:

  • it opens the page on localhost,
  • enters edit mode,
  • switches to PDF mode,
  • enforces the requested book/article, paper, font, ToC, and 2-up spread settings,
  • opens the same print-ready popup used by the UI,
  • and saves that popup with Chromium page.pdf().

When --site-root is provided, the script starts both:

  • node scripts/local-markdown-bridge.mjs --content-root <site-root> --port <bridge-port>
  • hugo server --source <site-root> --themesDir /home/henry/Code --port <hugo-port> ...

so the command can be run directly against a local Hugo project without manually starting the browser workflow first.

The script logs:

  • browser console messages,
  • uncaught page errors,
  • failed requests,
  • startup/navigation failures.

Browser WYSIWYG Smoke

For edit-mode regression checks, use:

node scripts/check-edit-wysiwyg.js http://127.0.0.1:1314/about/
node scripts/check-section-class-contract.mjs

This smoke is intentionally stronger than a visibility check. After clicking the first live content block, it now asserts:

  • an active editor surface exists (in_place or safe replace_main fallback),
  • the visible content does not disappear into a hidden live mount,
  • a typed probe token changes NT_GetMarkdown() in the active session.

This is the current guard against the “click text and it goes blank instead of becoming editable” regression.

check-section-class-contract.mjs is the shared render-side guard for section/media ownership. It verifies that content-only sections do not leak media-only tokens like halfscreen, orient-*, or *-figure-fade-in, while real media sections still retain the section-level size/orientation tokens the shared layout CSS expects.

check-figure-position-contract.mjs is the shared guard for figure-owned placement. It verifies that canonical position-* classes remain on <figure> elements, do not leak back onto section roots, and compile to direct figure.position-* CSS selectors.

check-head-asset-contract.mjs is the shared head/runtime guard. It verifies that local pages emit the expected stylesheet/runtime chain and that the linked assets actually return 200:

  • /css/style.css
  • fingerprinted variables.css
  • /css/brochure.css where brochure mode is enabled
  • prefetch/prefetch.js
  • editor/nowtype.min.js
  • custom/toggleMarkdown.js

check-responsive-layout.mjs is the shared mobile/layout guard. It checks Rawson and TutorLumin at a phone viewport and currently asserts:

  • no horizontal page overflow,
  • mobile header nav stays on rows instead of collapsing into a column,
  • shared fit figures/iframe embeds fill the content width on routes like /bookings/ and /hall-layout/,
  • Chris homepage fixed chrome still leaves the footer visible on first load.

check-structured-block-editors.mjs is the shared modal-editor smoke for structured brochure blocks. It currently opens and closes the live page editors for:

  • team
  • gallery
  • contact
  • testimonials
  • faq
  • booking

check-tutorlumin-student-platform.mjs is the integrated TutorLumin smoke for the current student-facing shared widgets. It verifies:

  • /fundalumin/ renders live Fundalumin campaign cards and wishlist items,
  • /calendar/ renders the shared public timetable with room filters and date cells,
  • /enrol/ renders the shared booking widget with a real week-strip and bookable slots,
  • optional --reserve mode can also create a real local hosted checkout session through booking + payments

check-pswp-spa.mjs is the shared PhotoSwipe + SPA regression. It verifies the real failure path:

  • navigate to /gallery/ through the SPA
  • confirm structured gallery items still carry data-pswp-width / data-pswp-height
  • confirm the first image click opens PhotoSwipe instead of falling through to the raw image URL

The responsive smoke now prefers a sandbox-safe Chromium launch (--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage) before falling back to Firefox, so local CI/sandbox runs do not depend on Firefox surviving large Rawson pages.

Structured gallery blocks now render PhotoSwipe-ready anchors server-side, and the shared PhotoSwipe init treats bare .pswp-item links as valid children with inferred dimensions. That keeps the first gallery click working after SPA navigation instead of depending on late client-side wrapping.

The shared header.style1 mobile contract now also treats #special as a full-row CTA on narrower widths, and layouts/partials/logo.html no longer emits an empty .logo anchor when a site does not actually define a logo/title/home-icon payload.

The shared section contract now distinguishes three render flows:

  • content_only: no section media, so figure-only section tokens are stripped; homepage lead sections and explicitly authored fullscreen / halfscreen can still keep screen-size section tokens
  • figure_led: section media exists but uses figure-led layouts like .fit, so split-screen size/orientation defaults are stripped unless the authored section attrs explicitly request split semantics
  • split: section media participates in split banner/spotlight layout, so section-level halfscreen/fullscreen and orient-* tokens remain canonical

Rendered section roots now also expose that shared flow as a single hook class:

  • ql-flow-content-only
  • ql-flow-figure-led
  • ql-flow-split

Rendered section roots now also carry a neutral shared shell class:

  • ql-section

That neutral shell is where the shared framework now hangs ordinary section rhythm and content-only behavior, so canonical width/spacing/fullscreen handling is no longer duplicated separately in every legacy family selector.

The first neutral section-axis slice is now also emitted directly on rendered section roots and in #editorData. Every resolved section now carries:

  • flow
  • media_side
  • size
  • content_align
  • surface
  • layout_family

Those values currently surface as:

  • data-ql-flow
  • data-ql-media-side
  • data-ql-size
  • data-ql-content-align
  • data-ql-section-surface
  • data-ql-layout-family

This is intentionally additive. banner / spotlight / wrapper classes still render exactly as before, but Hugo now emits a smaller semantic contract that the editor and future SCSS can move to without rediscovering section meaning from legacy class bundles.

The first additive SCSS migration is now also in place for the smallest safe slice:

  • data-ql-flow="figure_led"
  • data-ql-size="full" | "half"
  • data-ql-section-surface="intro_card"

Those hooks now alias the existing shared figure_led, full/half, and intro-card selectors in _wrapper.scss, _banner.scss, and _spotlight.scss. Legacy classes still remain in place as the compatibility path while the rest of the section geometry migrates.

Explicitly authored section attrs now also count as an intentional section frame, even when the section body is empty. That keeps effect-only sections such as a pure .fx-starfield banner renderable after a --- split, instead of silently discarding the section or treating the attr line itself as paragraph content.

Shared viewport metrics now come from cdn/custom/layoutMetrics.js, imported through head/import/custom.html. It publishes --ql-main-offset-top and --ql-main-viewport-height on :root, which the shared section/effect SCSS can use for “fill the rest of the viewport after the header” layouts without site-specific JS.

figure_led sections now preserve promoted or base layout families when present instead of being forced back to wrapper. Explicit section attrs also win over figure-led inference: if an authored section declares banner / spotlight with orient-left or orient-right, the shared contract keeps that section on the split-flow path even when the figure itself carries .fit. Shared wrapper styling still uses ql-flow-figure-led to widen figure-led sections like /hall-layout/ without changing prose-width behavior for ordinary inline figures on single pages.

Canonical intro-card sections and the temporary .ql-hero / .ql-hero-card compatibility path are now driven by shared intro-card mixins in assets/scss/main.scss, with banner / spotlight consuming that shared behavior instead of carrying separate copies of the same content-only rules.

For deeper browser diagnosis, window.__qlNowtypePdfSmoke.getEditorState() now exposes the current editor/runtime state used by the smoke harness:

  • presentation and editor kind
  • current file/current page file
  • live-main attach/focus flags
  • live-main index summary
  • current live-main selection summary

Open browser.log after the run and inspect the first errors.

WebDAV mode (production)

The theme now supports a minimal setup: set one Hugo param and include one partial.

In hugo.toml:

[params]
  editor = true
  editorWebdavBase = "https://example.com/webdav/content/"

In your <head> (if you are not already using this theme’s default head.html):

{{ $cdn := partial "helpers/cdn.html" . }}
{{ partial "head/import/markdown-editor" (dict "Page" . "CDN" $cdn) }}

If you need advanced options, you can still set global config before toggleMarkdown.js runs:

<script>
  window.QLMarkdownEditor = {
    defaultMode: "webdav",
    webdavBase: "https://example.com/webdav/content/",
    webdavAuth: { username: "user", password: "pass" },
    nowtypePageSrc: "/cdn/editor/web.demo.html", // used as asset-base reference
    nowtypeEnabled: true,
    nowtypeSrc: "https://cdn.quantalumin.com/editor/nowtype.min.js"
  };
</script>

Optional cache-bust: set params.qlMarkdownEditor.assetVersion (or params.assetVersion) and the editor scripts will append ?v=... automatically. When no explicit version is set, the shared head chain now adds development-only ?v=... query params to local /css/... and /cdn/... assets through helpers/asset-url.html, so an ordinary refresh on localhost picks up fresh CSS/JS without needing a hard reload. The same version is still applied to the service-worker registration URL, and localhost/127.0.0.1 sessions automatically unregister old service workers and clear stale caches on first load so editor/runtime fixes are not masked by cached assets.

Mode can also be switched in the overlay UI.

Attributions

  • Font-Awesome icons detached from their repo for easier importing of this theme

TutorLumin partners with QuantaLumin

You’re connecting to your QuantaLumin account on members.quantalumin.com.