Files
Youssef 7a890a365d Carnival rework, sharper anti-slop guards, and three new worked examples
Carnival: fixed the three palette drops whose second accent failed contrast,
tightened the display tracking, and added section-rhythm, alignment, and
drop-selection guidance so the theme lands well-aligned and readable by
default. Lightened and tightened the Carnival headers on the landing.

Anti-slop guards (skill references): heading-to-body alignment coherence as a
principle rather than a rigid rule; centered overlays and command palettes must
center via their container; and a hero headline is never only a number, a lead
figure always pairs with words.

Worked Examples: added three generated pages to the landing carousel, a
small-batch honey farm, a risograph print fair, and an experimental type
studio, after fixing their nav centering and section alignment.

Media: removed the Soroe example card along with the unused scroll-videos,
poster stills, and orphaned thumbnails it left behind, and bumped the asset
cache-bust so the restyled CSS reloads.
2026-06-03 15:27:32 +01:00

140 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* gen-13 · Bríd Halloran — Press Quaternary
* Three small, purposeful behaviours:
* 1. the live variable-font specimen (range axes drive font-variation-settings)
* 2. a scroll-spy that marks the current section dot in the side-rail
* 3. an optimistic enquire form with the full set of states
* All motion respects prefers-reduced-motion.
*/
(function () {
"use strict";
/* ── 1 · Living variable specimen ───────────────────────── */
var word = document.getElementById("liveWord");
var fig = document.querySelector(".specimen-live");
var axes = [
{ input: document.getElementById("axWght"), out: document.getElementById("wghtVal"), prop: "--w" },
{ input: document.getElementById("axWdth"), out: document.getElementById("wdthVal"), prop: "--wd" },
{ input: document.getElementById("axOpsz"), out: document.getElementById("opszVal"), prop: "--op" }
];
function applyAxes() {
if (!word) return;
var w = axes[0].input ? axes[0].input.value : 320;
var wd = axes[1].input ? axes[1].input.value : 100;
var op = axes[2].input ? axes[2].input.value : 96;
// write the variation directly so it overrides the breathing keyframes
word.style.fontVariationSettings =
"'wght' " + w + ", 'wdth' " + wd + ", 'opsz' " + op;
axes.forEach(function (a) {
if (a.out && a.input) a.out.textContent = a.input.value;
});
}
axes.forEach(function (a) {
if (!a.input) return;
a.input.addEventListener("input", function () {
if (fig) fig.dataset.grabbed = "true"; // stop the ambient breathe loop
applyAxes();
});
});
// initialise the value read-outs (breathing loop owns the glyph until first grab)
axes.forEach(function (a) { if (a.out && a.input) a.out.textContent = a.input.value; });
/* ── 2 · Scroll-spy for the rail dots ───────────────────── */
var dotLinks = Array.prototype.slice.call(
document.querySelectorAll(".rail__dots a")
);
var sections = dotLinks
.map(function (a) {
var id = a.getAttribute("href").slice(1);
return { link: a, el: document.getElementById(id) };
})
.filter(function (s) { return s.el; });
if ("IntersectionObserver" in window && sections.length) {
var spy = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
var match = sections.find(function (s) { return s.el === entry.target; });
if (!match) return;
dotLinks.forEach(function (l) {
var on = l === match.link;
l.classList.toggle("is-current", on);
if (on) { l.setAttribute("aria-current", "true"); }
else { l.removeAttribute("aria-current"); }
});
});
},
{ rootMargin: "-45% 0px -50% 0px", threshold: 0 }
);
sections.forEach(function (s) { spy.observe(s.el); });
}
/* ── 3 · Enquire form — optimistic, all states ──────────── */
var form = document.getElementById("enquire");
if (form) {
var input = document.getElementById("enq-email");
var help = document.getElementById("enq-help");
var btn = form.querySelector(".enquire__btn");
var label = btn ? btn.querySelector(".btn__label") : null;
var defaultHelp = help ? help.textContent : "";
var touched = false;
function setHelp(text, tone) {
if (!help) return;
help.textContent = text;
if (tone) help.setAttribute("data-tone", tone);
else help.removeAttribute("data-tone");
}
function validEmail(v) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim());
}
function validate() {
if (!input) return false;
var ok = validEmail(input.value);
input.setAttribute("aria-invalid", ok ? "false" : "true");
return ok;
}
if (input) {
input.addEventListener("blur", function () {
touched = true;
if (input.value && !validate()) {
setHelp("That address is missing an @ or a domain. Check it and try again.", "error");
} else {
setHelp(defaultHelp, null);
}
});
input.addEventListener("input", function () {
if (!touched) return;
if (validate()) setHelp(defaultHelp, null);
});
}
form.addEventListener("submit", function (e) {
e.preventDefault();
touched = true;
if (!validate()) {
setHelp("That address is missing an @ or a domain. Check it and try again.", "error");
if (input) input.focus({ preventScroll: true });
return;
}
// optimistic: show loading, then resolve to a quiet success (no toast)
if (btn) { btn.dataset.state = "loading"; btn.disabled = true; }
if (label) label.textContent = "Sending…";
setHelp("Sending your note…", null);
window.setTimeout(function () {
if (btn) { btn.dataset.state = "success"; btn.disabled = false; }
if (label) label.textContent = "Note sent ✓";
if (input) { input.value = ""; input.setAttribute("aria-invalid", "false"); }
setHelp("Thanks — your note is on its way. Ill reply from studio@pressquaternary within a few days.", "success");
}, 900);
});
}
})();