![]()
diff --git a/layouts/_partials/landing-body.html b/layouts/_partials/landing-body.html
index 7a781cc..14a77c0 100644
--- a/layouts/_partials/landing-body.html
+++ b/layouts/_partials/landing-body.html
@@ -39,6 +39,14 @@
Telar
+
+
+
What is Telar?¿Qué es Telar?
+
Telar (Spanish for "loom") weaves together images, video, audio, narrative text, and layered contextual information into interactive visual narrative exhibitions. It follows minimal computing principles: plain text authoring, static generation, and sustainable hosting.
+
Telar entreteje imágenes, video, audio, texto narrativo e información contextual en exhibiciones narrativas visuales e interactivas. Sigue los principios de computación mínima: autoría en texto plano, generación estática y alojamiento sostenible.
+
+
+
{{ partial "homepage-demo.html" . }}
diff --git a/static/assets/homepage-demo/README.md b/static/assets/homepage-demo/README.md
index 2456bfd..33f796f 100644
--- a/static/assets/homepage-demo/README.md
+++ b/static/assets/homepage-demo/README.md
@@ -1,6 +1,6 @@
# Telar homepage video
The homepage uses normal document flow and the supplied Spanish/English Vimeo
-embeds. The canonical definition sits above a full-width film at every screen size. There is no native scroll-film renderer or local MP4 fetch.
+embeds. The definition and viewport-fitted film occupy separate scroll steps. There is no native scroll-film renderer or local MP4 fetch.
Only the active-language poster is loaded initially. IntersectionObserver creates
the iframe and loads Vimeo's SDK as the film enters view. Playback is muted and
@@ -10,8 +10,12 @@ The progress bar and auxiliary player controls are disabled. Reduced motion supp
uses Vimeo's own controls. A failed load retains the poster and offers a Vimeo
link. The supplied video IDs and privacy hashes are preserved.
-Lenis and scroll-motion.js provide bounded page smoothing and one section per
-arrow press. Film bookmarks and the pinned 2500svh section have been removed.
+Lenis and scroll-motion.js animate one stop per wheel/trackpad gesture or arrow
+press. A 200ms quiet gap separates wheel gestures; momentum cannot trigger a
+second move. Tall sections include reading stops to keep every row reachable.
+Native snap points cover touch input. For wheel input inside Vimeo, a scroll
+observer hands native movement to the same step controller without covering the
+player or disabling its controls. Reduced motion uses immediate transitions. Film bookmarks and the pinned 2500svh section have been removed.
The approved palette is terracotta, cream, light grey, charcoal and darker
secondary text, with a blue project section. The larger loom and original wordmark scale are retained. All developer/supporter logos, including UT AMPL, remain.
diff --git a/static/assets/homepage-demo/homepage-demo.css b/static/assets/homepage-demo/homepage-demo.css
index 30c73be..3c6bf0c 100644
--- a/static/assets/homepage-demo/homepage-demo.css
+++ b/static/assets/homepage-demo/homepage-demo.css
@@ -1,6 +1,9 @@
/* Approved homepage palette and normal-flow Vimeo demonstration. */
:root{--text-medium:#596776;--demo-background:#f4f5f6}
-html{scroll-snap-type:none;scroll-behavior:auto}
+html{scroll-snap-type:y mandatory;scroll-behavior:auto}
+html.is-stepping{scroll-snap-type:none}
+main > section,body > footer,.telar-reading-stop{scroll-snap-align:start;scroll-snap-stop:always}
+.telar-reading-stop{position:absolute;left:0;width:1px;height:1px;opacity:0;pointer-events:none}
body{overflow-x:clip}
.visually-hidden,.skip-link:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
.skip-link:focus{position:fixed;top:12px;left:12px;z-index:2000;padding:12px 18px;background:var(--cream);color:var(--terracotta);border-radius:8px}
@@ -17,11 +20,13 @@ a:focus-visible,button:focus-visible,iframe:focus-visible{outline:3px solid var(
.hero__utility a{color:var(--cream);font-size:15px;text-decoration:underline;text-underline-offset:4px;opacity:.9}
.hero__utility a:hover{opacity:1}
.hero__scroll{bottom:20px}
-.telar-demo-section{background:var(--demo-background);min-height:auto;padding:64px 24px}
-.telar-demo-layout{display:grid;grid-template-columns:1fr;gap:32px;max-width:1600px;width:100%;margin:auto}
-.telar-about-copy{display:grid;grid-template-columns:minmax(280px,.8fr) minmax(0,1.7fr);gap:clamp(24px,4vw,64px);align-items:start;background:transparent;box-shadow:none;border:0;padding:0}
-.telar-about-copy h2{margin-bottom:0}
-.telar-about-copy p{font-weight:400;line-height:1.65;margin-bottom:0}
+.telar-about-section{background:var(--demo-background);min-height:100svh;padding:60px 24px}
+.telar-demo-section{background:var(--demo-background);min-height:100svh;padding:24px}
+/* Fit the complete film inside one viewport, including on short desktop windows. */
+.telar-demo-layout{width:100%;max-width:min(1600px,calc((100svh - 48px) * 16 / 9));margin:auto}
+.telar-about-copy{background:transparent;box-shadow:none;border:0;padding:0;width:100%;max-width:900px;margin:0 auto}
+.telar-about-copy h2{margin-bottom:24px;text-align:center}
+.telar-about-copy p{font-weight:400;line-height:1.65;margin-bottom:0;font-size:20px}
.telar-demo-media{margin:0;min-width:0}
/* The Vimeo exports have 109px of blank padding above and below the
1920x1080 film. Crop that padding without cropping any film content.
@@ -49,18 +54,16 @@ a:focus-visible,button:focus-visible,iframe:focus-visible{outline:3px solid var(
.landing-footer a{color:var(--cream);text-underline-offset:3px}
.landing-footer a:hover{color:var(--gold)}
@media(max-width:900px){
- .telar-demo-layout{grid-template-columns:1fr;gap:24px;max-width:900px}
- .telar-demo-section{padding:64px 24px}
- .telar-about-copy{grid-template-columns:1fr;gap:12px;max-width:900px}
+ .telar-demo-section{min-height:auto}
+ .telar-about-section{min-height:auto;padding:64px 24px}
.telar-demo-media{width:100%}
}
@media(max-width:600px){
.hero{padding:24px 20px 68px}
.hero__lang-toggle button{min-height:44px}
.hero__utility{gap:4px 18px}
- .telar-demo-section{padding:48px 16px}
- .telar-about-copy{padding:0 4px}
- .telar-demo-layout{gap:16px}
+ .telar-demo-section{padding:24px 16px}
+ .telar-about-section{padding:48px 20px}
.cta-section{padding:56px 20px}
.features__grid{gap:16px}
}
diff --git a/static/assets/homepage-demo/homepage-demo.js b/static/assets/homepage-demo/homepage-demo.js
index 3c28a91..1cd076e 100644
--- a/static/assets/homepage-demo/homepage-demo.js
+++ b/static/assets/homepage-demo/homepage-demo.js
@@ -1,18 +1,26 @@
-/* Normal page flow with bounded Lenis smoothing. Vimeo loads only on approach. */
+/* Stepped page navigation with bounded transitions. Vimeo loads only on approach. */
(() => {
'use strict';
const motion=matchMedia('(prefers-reduced-motion: reduce)');
const small=matchMedia('(max-width: 900px)');
const travel=new TelarScrollMotion(scrollY);
const limit=()=>Math.max(0,document.documentElement.scrollHeight-innerHeight);
- const smooth=()=>!motion.matches&&!small.matches;
+ const smooth=()=>!motion.matches;
+ let lastWheel=-Infinity,wheelTotal=0,wheelConsumed=false;
const lenis=new Lenis({lerp:.06,smoothWheel:!motion.matches,wheelMultiplier:.65,autoRaf:false,anchors:false,
virtualScroll({deltaY,event}){
- if(!smooth()||event.type!=='wheel'||event.ctrlKey)return;
+ if(event.type!=='wheel'||event.ctrlKey||event.metaKey)return;
if(event.target.closest('input,textarea,select,iframe,[data-lenis-prevent]'))return false;
- event.preventDefault();pendingTarget=null;
- if(!travel.active)travel.reset(scrollY);
- travel.wheel(deltaY,innerHeight,limit());return false;
+ event.preventDefault();
+ const now=performance.now();
+ if(now-lastWheel>200){wheelTotal=0;wheelConsumed=false}
+ lastWheel=now;
+ // One gesture owns one move. Its momentum tail must not start another.
+ if(wheelConsumed||travel.active){wheelConsumed=true;return false}
+ if(Math.sign(wheelTotal)!==Math.sign(deltaY))wheelTotal=0;
+ wheelTotal+=deltaY;
+ if(Math.abs(wheelTotal)>=12){wheelConsumed=true;advance(Math.sign(wheelTotal))}
+ return false;
}
});
let raf=0,lastTick=0,pendingTarget=null,onArrival=null;
@@ -20,19 +28,24 @@
const dt=lastTick?(time-lastTick)/1000:0;lastTick=time;
if(travel.active){
lenis.scrollTo(travel.step(dt,innerHeight,limit()),{immediate:true});
- if(!travel.active){pendingTarget=null;const arrived=onArrival;onArrival=null;arrived?.()}
+ if(!travel.active){document.documentElement.classList.remove('is-stepping');pendingTarget=null;const arrived=onArrival;onArrival=null;arrived?.()}
}
lenis.raf(time);raf=requestAnimationFrame(tick);
}
raf=requestAnimationFrame(tick);
- function manualInput(){pendingTarget=null;onArrival=null;travel.reset(scrollY)}
+ function manualInput(){document.documentElement.classList.remove('is-stepping');pendingTarget=null;onArrival=null;travel.reset(scrollY)}
function preference(){manualInput();lenis.options.smoothWheel=!motion.matches;lenis.resize();if(motion.matches)suspend();else syncPlayback()}
+ // Read layout positions so fade-in transforms cannot create an extra arrow stop.
+ function layoutTop(element){
+ let top=0;for(;element;element=element.offsetParent)top+=element.offsetTop;return top;
+ }
function scrollTo(target,arrive=null){
if(typeof target==='string')target=document.querySelector(target);
- if(target instanceof Element)target=target.getBoundingClientRect().top+scrollY;
+ if(target instanceof Element)target=layoutTop(target);
if(!Number.isFinite(target))return;
target=Math.min(limit(),Math.max(0,target));onArrival=arrive;
- if(!smooth()){lenis.scrollTo(target,{immediate:true});travel.reset(target);pendingTarget=null;onArrival=null;arrive?.();return}
+ document.documentElement.classList.add('is-stepping');
+ if(!smooth()){lenis.scrollTo(target,{immediate:true});travel.reset(target);pendingTarget=null;onArrival=null;document.documentElement.classList.remove('is-stepping');arrive?.();return}
if(!travel.active)travel.reset(scrollY);
travel.moveTo(target,innerHeight);
}
@@ -41,17 +54,47 @@
if(!element.hasAttribute('tabindex'))element.setAttribute('tabindex','-1');
element.focus({preventScroll:true});
}
+ function stops(){
+ const points=[];
+ for(const section of document.querySelectorAll('main > section,body > section,body > footer')){
+ const top=layoutTop(section),overflow=section.offsetHeight-innerHeight;
+ const offsets=[];
+ if(!section.hidden){
+ points.push(top);
+ // Tall content gets reading stops too, so stepping never skips its lower rows.
+ if(overflow>64){
+ const parts=Math.ceil(section.offsetHeight/innerHeight)-1;
+ for(let i=1;i<=parts;i++)offsets.push(overflow*i/parts);
+ }
+ points.push(...offsets.map(offset=>top+offset));
+ }
+ // Native snapping also handles wheel events inside the cross-origin Vimeo frame.
+ // Keep identical stops available when those events cannot reach this document.
+ const existing=[...section.querySelectorAll('.telar-reading-stop')];
+ if(existing.length!==offsets.length||existing.some((node,i)=>parseFloat(node.style.top)!==offsets[i])){
+ existing.forEach(node=>node.remove());
+ for(const offset of offsets){
+ const marker=document.createElement('span');marker.className='telar-reading-stop';
+ marker.setAttribute('aria-hidden','true');marker.style.top=offset+'px';section.append(marker);
+ }
+ }
+ }
+ return [...new Set(points.map(n=>Math.min(limit(),Math.max(0,n))))].sort((a,b)=>a-b);
+ }
+ const stopObserver=new ResizeObserver(stops);
+ document.querySelectorAll('main > section,body > footer').forEach(section=>stopObserver.observe(section));
+ function advance(direction){
+ const ordered=stops(),current=pendingTarget??scrollY;
+ const target=direction>0?ordered.find(n=>n>current+8):ordered.slice().reverse().find(n=>n
section,body > section,body > footer')].filter(e=>!e.hidden).map(e=>e.getBoundingClientRect().top+scrollY);
- const ordered=[...new Set(stops.map(n=>Math.min(limit(),Math.max(0,n))))].sort((a,b)=>a-b);
- const current=pendingTarget??scrollY,target=direction>0?ordered.find(n=>n>current+8):ordered.slice().reverse().find(n=>n{
+ // Cross-origin iframe wheel events do not bubble. Observe their native movement
+ // and hand the gesture to the same step controller, keeping Vimeo clickable.
+ if(!embed.querySelector('iframe')?.matches(':hover')){
+ if(!travel.active)travel.reset(scrollY);return;
+ }
+ const delta=scrollY-travel.position;if(Math.abs(delta)<2)return;
+ const now=performance.now();
+ if(now-lastWheel>200){wheelTotal=0;wheelConsumed=false}
+ lastWheel=now;
+ if(wheelConsumed||travel.active){
+ wheelConsumed=true;lenis.scrollTo(travel.position,{immediate:true});return;
+ }
+ const previous=travel.position;wheelConsumed=true;
+ travel.reset(scrollY);pendingTarget=previous;advance(Math.sign(delta));
+ },{passive:true});
const languages=JSON.parse(document.getElementById('telar-demo-config').textContent);
let sdkPromise=null,player=null,iframe=null,ready=false,playing=false,inView=false,userPaused=false,pauseExpected=false,generation=0,timeout=0,data;
function sdk(){