site/motion.js
Rama main · réplica de lectura del repositorio original.
/**
* hired.tools — the page's behaviour.
*
* Rules this file keeps, because the product keeps them:
*
* 1. Nothing here is required to read the page. Every string that gets typed,
* every element that fades in and every number that counts up is already in
* the HTML. The `js` class added in the document head is what opts an
* element into being hidden first — so with scripting off, or before this
* file arrives, the page is simply finished.
* 2. Nothing loops. Animations play once, on entry, and then hold.
* 3. Anyone who has asked their system to calm down gets a still page. The
* stylesheet neutralises the transitions; `calm` below skips the work.
*/
(function () {
"use strict";
var calm = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var $ = function (sel, root) { return (root || document).querySelector(sel); };
var $$ = function (sel, root) {
return Array.prototype.slice.call((root || document).querySelectorAll(sel));
};
/** Run fn the first time el crosses into view, then stop watching it. */
function once(el, fn, margin) {
if (!("IntersectionObserver" in window)) { fn(el); return; }
var io = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
io.unobserve(entry.target);
fn(entry.target);
});
},
{ rootMargin: margin || "0px 0px -12% 0px", threshold: 0.15 }
);
io.observe(el);
}
// -------------------------------------------------------------------------
// One pointer, one frame
//
// A dozen things on this page answer the cursor: two marks, the bento cards,
// the primary buttons, the two files. Each used to attach its own
// `pointermove` and read a bounding box inside it, and a read that happens
// after somebody else's write is a forced layout — so moving the mouse cost
// twelve of them, every event, which is exactly the kind of thing that makes
// a page feel like it is dragging.
//
// They share one listener and one frame now: every rect is read together,
// then every style is written together. One layout per frame, whatever is
// watching.
// -------------------------------------------------------------------------
var watchers = [];
var cursor = { x: 0, y: 0, near: false };
var pointerFrame = 0;
function runWatchers() {
pointerFrame = 0;
// Reads first, all of them.
for (var i = 0; i < watchers.length; i++) {
watchers[i].box = watchers[i].el.getBoundingClientRect();
}
// Then the writes, which cannot invalidate a read that has already happened.
for (var j = 0; j < watchers.length; j++) {
watchers[j].apply(watchers[j].box, cursor);
}
}
function schedule() {
if (!pointerFrame) pointerFrame = requestAnimationFrame(runWatchers);
}
/** Answer the pointer. `apply` is handed the element's rect and the cursor. */
function follows(el, apply) {
if (calm) return;
watchers.push({ el: el, apply: apply, box: null });
if (watchers.length === 1) {
window.addEventListener(
"pointermove",
function (event) {
cursor.x = event.clientX;
cursor.y = event.clientY;
cursor.near = true;
schedule();
},
{ passive: true }
);
window.addEventListener("pointerleave", function () {
cursor.near = false;
schedule();
});
}
}
/** -1..1 across a box grown by `reach` times its own size. */
function offset(box, point, axis, reach) {
var mid = axis === "x" ? box.left + box.width / 2 : box.top + box.height / 2;
var span = (axis === "x" ? box.width : box.height) * (reach || 1);
if (!span) return 0;
return Math.max(-1, Math.min(1, (point - mid) / span));
}
// -------------------------------------------------------------------------
// The mark, built out into an object
//
// The markup ships the flat SVG. This puts a preserve-3d stage beside it and
// only then adds `on`, which is what hides the picture — so the mark is the
// mark with scripting off, and this file failing to load costs nothing.
//
// Geometry is the same 64-unit grid the SVG uses, expressed as fractions of
// --s so one element scales to any size: bar height 10.9%, longest bar 50%,
// corner radius 23.4%, the three bars at 26.6%, 44.5% and 62.5% down.
// -------------------------------------------------------------------------
var BARS = [
{ y: 0.265625, w: 0.203125 },
{ y: 0.4453125, w: 0.3515625 },
{ y: 0.625, w: 0.5 },
];
/** Slices through the slab. Enough that the rim reads as milled, not stepped. */
var SLICES = 14;
function buildMark(host) {
var size = parseFloat(host.getAttribute("data-mark3d")) || 24;
host.style.setProperty("--s", size + "px");
var depth = size * 0.24;
var stage = document.createElement("span");
stage.className = "m3d-stage";
var slab = document.createElement("span");
slab.className = "m3d-slab";
for (var i = 0; i < SLICES; i++) {
var slice = document.createElement("i");
slice.style.setProperty("--z", (i * depth) / SLICES);
slab.appendChild(slice);
}
stage.appendChild(slab);
BARS.forEach(function (spec, index) {
var bar = document.createElement("span");
bar.className = "m3d-bar";
bar.style.setProperty("--y", spec.y);
bar.style.setProperty("--w", spec.w);
bar.style.setProperty("--bi", index);
var grow = document.createElement("span");
for (var j = 0; j < 4; j++) {
var slice = document.createElement("i");
slice.style.setProperty("--z", j * size * 0.009);
grow.appendChild(slice);
}
bar.appendChild(grow);
stage.appendChild(bar);
});
var glass = document.createElement("span");
glass.className = "m3d-glass";
stage.appendChild(glass);
host.appendChild(stage);
host.classList.add("on");
// The field is the mark's own region grown by four times its size, so the
// cursor is answered well before it arrives rather than only on top of the
// 24px target itself.
follows(host, function (box, point) {
if (!box.width) return;
var dx = point.near ? offset(box, point.x, "x", 4) : 0;
var dy = point.near ? offset(box, point.y, "y", 4) : 0;
stage.style.setProperty("--ry", -23 + dx * 14 + "deg");
stage.style.setProperty("--rx", 12 - dy * 14 + "deg");
glass.style.setProperty("--gx", 34 - dx * 30 + "%");
glass.style.setProperty("--gy", 24 - dy * 22 + "%");
});
once(host, function (target) { target.classList.add("in"); });
}
$$("[data-mark3d]").forEach(buildMark);
// -------------------------------------------------------------------------
// The headline, a word at a time
//
// Each word gets a box that clips it and an inner span that rises out of the
// box. Element children — the accented span in the hero headline — are
// wrapped whole rather than taken apart, so the accent survives and any
// markup inside it comes along.
// -------------------------------------------------------------------------
$$("[data-words]").forEach(function (line) {
var pieces = [];
Array.prototype.slice.call(line.childNodes).forEach(function (node) {
if (node.nodeType === 3) {
node.textContent.split(/(\s+)/).forEach(function (chunk) {
if (!chunk.trim()) { if (chunk) pieces.push(document.createTextNode(chunk)); return; }
var inner = document.createElement("span");
inner.textContent = chunk;
pieces.push(inner);
});
} else if (node.nodeType === 1) {
pieces.push(node);
}
});
if (!pieces.length) return;
line.textContent = "";
var i = 0;
pieces.forEach(function (piece) {
if (piece.nodeType === 3) { line.appendChild(piece); return; }
var box = document.createElement("span");
box.className = "w";
box.style.setProperty("--wi", i++);
box.appendChild(piece);
line.appendChild(box);
});
line.classList.add("split");
once(line, function (target) { target.classList.add("in"); }, "0px");
});
// -------------------------------------------------------------------------
// Pointer light on a card, and the pull on the primary action
//
// Both write a custom property and stop. No layout is read on move except
// the one rect, and neither runs at all for anyone who asked for calm.
// -------------------------------------------------------------------------
$$("[data-spot]").forEach(function (card) {
/* A card that leans towards the cursor. Two degrees is the whole range:
enough that it reads as an object being looked at, not enough for a
paragraph on it to start keystoning. The axis is perpendicular to the
direction of the cursor, which is what makes it lean *towards* it rather
than about one edge — and it goes on `rotate`, not `transform`, because
the reveal owns `transform` on these elements. */
var leans = card.hasAttribute("data-lean");
follows(card, function (box, point) {
card.style.setProperty("--mx", point.x - box.left + "px");
card.style.setProperty("--my", point.y - box.top + "px");
if (!leans || !box.width) return;
var over =
point.near &&
point.x >= box.left && point.x <= box.right &&
point.y >= box.top && point.y <= box.bottom;
if (!over) {
card.style.rotate = "";
card.style.translate = "";
return;
}
var dx = (point.x - (box.left + box.width / 2)) / (box.width / 2);
var dy = (point.y - (box.top + box.height / 2)) / (box.height / 2);
var reach = Math.min(1, Math.sqrt(dx * dx + dy * dy));
// Dead centre there is no direction to lean in, and an axis of zero
// length is not a rotation the browser can make sense of.
card.style.rotate = reach < 0.02 ? "none" : -dy + " " + dx + " 0 " + (reach * 2).toFixed(2) + "deg";
card.style.translate = "0 -2px";
});
});
/* A card that leans very slightly towards the cursor. Two and a half degrees
is the whole range: enough that the card reads as an object being looked
at, not so much that a paragraph on it starts to keystone. */
$$("[data-tilt]").forEach(function (host) {
var stage = host.firstElementChild;
if (!stage) return;
var lean = Number(host.getAttribute("data-tilt")) || 6;
follows(host, function (box, point) {
if (!box.width) return;
var dx = point.near ? offset(box, point.x, "x", 1.6) : 0;
var dy = point.near ? offset(box, point.y, "y", 1.6) : 0;
stage.style.setProperty("--ry", -9 + dx * lean + "deg");
stage.style.setProperty("--rx", 5 - dy * lean + "deg");
});
});
$$("[data-magnet]").forEach(function (button) {
var pull = Number(button.getAttribute("data-magnet")) || 5;
follows(button, function (box, point) {
if (!box.width) return;
var dx = (point.x - (box.left + box.width / 2)) / box.width;
var dy = (point.y - (box.top + box.height / 2)) / box.height;
// Outside its own reach it sits still. A button that leans towards a
// cursor on the other side of the page is a button that is never still,
// which is the opposite of the point.
var near = point.near && Math.abs(dx) < 1.1 && Math.abs(dy) < 1.6;
button.style.setProperty("--mgx", near ? dx * pull + "px" : "0px");
button.style.setProperty("--mgy", near ? dy * pull * 0.6 + "px" : "0px");
});
});
// -------------------------------------------------------------------------
// Drawings that keep going
//
// The four drawings in the bento used to play once on entry and hold, which
// made them illustrations of a thing rather than the thing happening. They
// loop now — a line filed and a highlight pulled out of it, a document
// rebuilt for a different posting, a card walking the board, a follow-up
// coming due.
//
// This is a deliberate exception to "nothing loops", and it is bounded three
// ways. Every cycle begins and ends at the drawing's resting state, so a
// paused one is indistinguishable from a still one. Nothing runs unless it is
// on screen — the class goes on when the drawing enters and comes off when it
// leaves, so four animations are not burning a phone's battery while somebody
// reads the FAQ five sections down. And `calm` never adds the class at all,
// which leaves the finished picture exactly as it was.
// -------------------------------------------------------------------------
if (!calm && "IntersectionObserver" in window) {
var living = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
entry.target.classList.toggle("live", entry.isIntersecting);
});
},
{ threshold: 0.2 }
);
$$("[data-live]").forEach(function (el) { living.observe(el); });
}
// -------------------------------------------------------------------------
// Reveals
// -------------------------------------------------------------------------
$$("[data-reveal]").forEach(function (el) {
once(el, function (target) { target.classList.add("in"); });
});
// -------------------------------------------------------------------------
// Nav and scroll progress
// -------------------------------------------------------------------------
var nav = $(".nav");
var progress = $(".progress");
var ticking = false;
/* The stylesheet drives the progress bar off a scroll timeline where the
browser supports one, which is both smoother and off the main thread.
Only fill it from here when it doesn't. */
var cssDrivesProgress =
window.CSS && CSS.supports && CSS.supports("animation-timeline: scroll()");
if (cssDrivesProgress) progress = null;
/* Nav links that point at a section on this page, paired with their target,
so scrolling can say which one you are in. Anything pointing elsewhere —
GitHub, the app — is left alone. */
var marks = $$(".nav-links a.link[href^='#']")
.map(function (link) {
var section = document.getElementById(link.getAttribute("href").slice(1));
return section ? { link: link, section: section } : null;
})
.filter(Boolean);
var marked = null;
function markSection() {
if (!marks.length) return;
/* The section you are "in" is the last one whose top has passed a line a
third of the way down the viewport — steadier than asking which is most
visible, because these sections differ wildly in height. */
var line = window.innerHeight / 3;
var current = null;
marks.forEach(function (m) {
if (m.section.getBoundingClientRect().top <= line) current = m;
});
if (current === marked) return;
if (marked) marked.link.removeAttribute("aria-current");
if (current) current.link.setAttribute("aria-current", "true");
marked = current;
}
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(function () {
ticking = false;
var y = window.scrollY || window.pageYOffset;
if (nav) nav.classList.toggle("stuck", y > 12);
if (progress) {
var height = document.documentElement.scrollHeight - window.innerHeight;
progress.style.setProperty("--p", height > 0 ? Math.min(1, y / height) : 0);
}
markSection();
});
}
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
// -------------------------------------------------------------------------
// Smoothed scrolling
//
// Every wheel notch and every key press moves a target; the page eases
// towards it. Trackpads included — an earlier pass left them alone on the
// theory that a precision device is already smooth, and the result was a page
// that behaved differently depending on what you happened to be holding.
// One behaviour, tuned once.
//
// Deliberately NOT the usual implementation of this, which translates a
// wrapper element and leaves the document's real scroll offset at zero. Four
// things here read that offset — the sticky tour chapters, the progress bar
// and two scroll timelines — and a transformed wrapper breaks all of them. So
// the scroll is real: window.scrollTo, once a frame, towards a target.
// Find-on-page, the back button and the scrollbar all go on working.
//
// Two things are left alone, on purpose: touch, where the platform's own
// momentum is better than anything written here, and a pane with its own
// scrollbar, which should scroll itself.
// -------------------------------------------------------------------------
if (!calm && window.matchMedia("(pointer: fine)").matches) {
(function () {
var target = 0;
var frame = 0;
var last = 0;
var gliding = false;
var root = document.documentElement;
/* How much of the remaining distance is closed every 16.7ms. Lower is
longer and glassier; higher is tighter and more like the browser. This
is the one number worth arguing with. */
var EASE = 0.115;
function limit() {
return Math.max(0, root.scrollHeight - window.innerHeight);
}
function step(now) {
var gap = Math.min(64, now - last);
last = now;
var current = window.scrollY;
var left = target - current;
if (Math.abs(left) < 0.4) {
window.scrollTo(0, target);
stop();
return;
}
// Frame-rate independent, or 120Hz arrives twice as fast as 60.
window.scrollTo(0, current + left * (1 - Math.pow(1 - EASE, gap / 16.67)));
frame = requestAnimationFrame(step);
}
function stop() {
frame = 0;
gliding = false;
// Hand the anchors back their smooth scrolling.
root.style.scrollBehavior = "";
}
function begin() {
if (gliding) return;
target = window.scrollY;
gliding = true;
last = performance.now();
// A CSS smooth scroll on every frame of our own would fight this.
root.style.scrollBehavior = "auto";
}
function push(amount) {
begin();
target = Math.max(0, Math.min(limit(), target + amount));
if (!frame) frame = requestAnimationFrame(step);
}
function goto(where) {
begin();
target = Math.max(0, Math.min(limit(), where));
if (!frame) frame = requestAnimationFrame(step);
}
/* Anything that moves the page other than this — a scrollbar drag, an
anchor, the browser restoring a position — becomes the new target, or
the next notch would yank it back to where the glide was heading. */
function resync() {
if (!gliding) target = window.scrollY;
}
window.addEventListener("scroll", resync, { passive: true });
window.addEventListener("resize", resync, { passive: true });
function ownScroller(node) {
while (node && node.nodeType === 1 && node !== document.body) {
var style = window.getComputedStyle(node);
if (/(auto|scroll)/.test(style.overflowY) && node.scrollHeight > node.clientHeight + 1) {
return true;
}
node = node.parentNode;
}
return false;
}
window.addEventListener(
"wheel",
function (event) {
if (event.ctrlKey || event.defaultPrevented) return;
if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) return;
if (ownScroller(event.target)) return;
var amount =
event.deltaY *
(event.deltaMode === 1 ? 40 : event.deltaMode === 2 ? window.innerHeight : 1);
if (!amount) return;
event.preventDefault();
push(amount);
},
{ passive: false }
);
/* The keys that scroll, so a page turned with the keyboard arrives the
same way one turned with the wheel does. Anything with its own idea of
what these keys mean — a field, a button, a <summary> — keeps it. */
var TYPING = /^(input|textarea|select)$/;
window.addEventListener("keydown", function (event) {
if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) return;
var on = event.target;
if (on && (TYPING.test((on.tagName || "").toLowerCase()) || on.isContentEditable)) return;
if (on && on.closest && on.closest("button, summary, a, [tabindex], [role='button']")) return;
if (ownScroller(on)) return;
var page = window.innerHeight * 0.88;
var line = 90;
var by = null;
var to = null;
if (event.key === "PageDown") by = page;
else if (event.key === "PageUp") by = -page;
else if (event.key === " " || event.key === "Spacebar") by = event.shiftKey ? -page : page;
else if (event.key === "ArrowDown") by = line;
else if (event.key === "ArrowUp") by = -line;
else if (event.key === "Home") to = 0;
else if (event.key === "End") to = limit();
else return;
event.preventDefault();
if (to !== null) goto(to);
else push(by);
});
})();
}
// -------------------------------------------------------------------------
// The transcript
//
// The text is already on the page; this retypes it. Each turn waits for the
// one before it, tool chips appear while the answer is still being written,
// and each chip goes green when its call "returns" — which is what actually
// happens in a client, and the reason the sequence is worth showing at all.
// -------------------------------------------------------------------------
function typeInto(el, done) {
var full = el.getAttribute("data-text") || el.textContent;
el.setAttribute("data-text", full);
var speed = Number(el.getAttribute("data-speed")) || 16;
el.textContent = "";
var caret = document.createElement("span");
caret.className = "caret";
el.appendChild(caret);
var i = 0;
(function step() {
// Type in small bites rather than one character per frame: a per-frame
// character is slower than anyone reads and makes long lines drag.
var bite = full.charAt(i) === " " ? 2 : 1;
i = Math.min(full.length, i + bite);
caret.insertAdjacentText("beforebegin", full.slice(i - bite, i));
if (i < full.length) {
setTimeout(step, speed);
} else {
caret.remove();
if (done) done();
}
})();
}
function playTape(tape) {
var steps = $$("[data-step]", tape).sort(function (a, b) {
return Number(a.getAttribute("data-step")) - Number(b.getAttribute("data-step"));
});
function next(index) {
if (index >= steps.length) {
tape.classList.add("played");
return;
}
var step = steps[index];
var body = $("[data-type]", step) || step;
var calls = $$(".call", step);
/* A step can spend a moment working before it says anything, which is
what a client connected over MCP actually does: the calls go out, they
come back, and only then is there an answer to write. `data-think` is
how long that takes; the element it shows ships hidden, so a page
without this file has the answer and no spinner. */
var think = Number(step.getAttribute("data-think")) || 0;
var thinking = $("[data-thinking]", step);
// Chips land while it is still working, and go green as each returns.
calls.forEach(function (call, i) {
setTimeout(function () {
call.classList.add("in");
setTimeout(function () { call.classList.add("done"); }, 420 + i * 160);
}, 260 + i * 150);
});
step.classList.add("in");
function speak() {
if (thinking) thinking.hidden = true;
typeInto(body, function () {
setTimeout(function () { next(index + 1); }, Number(step.getAttribute("data-pause")) || 480);
});
}
if (think && thinking) {
thinking.hidden = false;
setTimeout(speak, think);
} else {
speak();
}
}
next(0);
}
$$("[data-tape]").forEach(function (tape) {
if (calm) {
$$(".call", tape).forEach(function (c) { c.classList.add("in", "done"); });
$$("[data-step]", tape).forEach(function (s) { s.classList.add("in"); });
$$("[data-thinking]", tape).forEach(function (t) { t.hidden = true; });
return;
}
/* Hide the answer text until its turn comes, without hiding the layout it
occupies — the panel must not change height while it plays, or every
section below it walks down the page a line at a time while somebody is
reading. Measure the finished line, hold that height, then empty it.
Measured again once the webfont has landed, because Inter is wider than
the fallback and a height reserved against the wrong face is the wrong
height. Only for a tape that has not started: re-measuring one mid-type
would throw away what it had written. */
var lines = $$("[data-type]", tape);
function reserve() {
lines.forEach(function (el) {
var full = el.getAttribute("data-text");
var showing = el.textContent;
el.style.minHeight = "";
el.textContent = full;
el.style.minHeight = el.getBoundingClientRect().height + "px";
el.textContent = showing;
});
}
lines.forEach(function (el) { el.setAttribute("data-text", el.textContent); });
reserve();
lines.forEach(function (el) { el.textContent = ""; });
var started = false;
if (document.fonts && document.fonts.ready) {
document.fonts.ready.then(function () { if (!started) reserve(); });
}
once(tape, function (target) {
started = true;
playTape(target);
}, "0px 0px -20% 0px");
});
// -------------------------------------------------------------------------
// Product shots, drawn at a real app width and scaled to fit
//
// Reflowing a screenshot into a narrow column stops it being a screenshot of
// anything — the type goes big relative to the chrome and the columns you
// are meant to see disappear. So each mock is laid out at the width the app
// is actually used at and then scaled down as one object, which is what a
// photograph of the screen would have done.
// -------------------------------------------------------------------------
var fitted = $$("[data-fit]");
// Below this, the app's own 12.5px rows stop being legible and the shot
// stops being worth showing. Past it we hold the scale and let the frame
// scroll sideways instead, which is what a phone can honestly do with a
// picture of a 1440px screen.
var FLOOR = 0.72;
function fit() {
fitted.forEach(function (el) {
var design = Number(el.getAttribute("data-fit")) || 1200;
var host = el.parentElement;
if (!host) return;
el.style.width = design + "px";
var scale = Math.max(FLOOR, Math.min(1, host.clientWidth / design));
el.style.transform = scale < 1 ? "scale(" + scale + ")" : "";
// offsetHeight ignores the transform, which is exactly what we need to
// work out how tall the scaled result is.
host.style.height = Math.round(el.offsetHeight * scale) + "px";
host.classList.toggle("pannable", design * scale > host.clientWidth + 1);
});
}
if (fitted.length) {
fit();
// Fonts land after first paint and change every height in here.
if (document.fonts && document.fonts.ready) document.fonts.ready.then(fit);
window.addEventListener("load", fit);
var refit;
var again = function () {
clearTimeout(refit);
refit = setTimeout(fit, 100);
};
window.addEventListener("resize", again);
// The board gets shorter when a card moves stage, and an image dropped into
// a slot changes everything. Watch the drawings rather than guess when.
if (window.ResizeObserver) {
var ro = new ResizeObserver(again);
fitted.forEach(function (el) { ro.observe(el); });
}
}
// -------------------------------------------------------------------------
// Monthly or annual
//
// Both figures are in the markup and the annual one ships hidden, so the page
// without this file is the monthly price and the control is not there to
// press. All this does is swap which of the two pairs is showing.
// -------------------------------------------------------------------------
$$("[data-plans]").forEach(function (group) {
var buttons = $$("button[data-period]", group);
var swappable = $$("[data-when]");
if (!buttons.length || !swappable.length) return;
group.hidden = false;
function show(period) {
buttons.forEach(function (button) {
var on = button.getAttribute("data-period") === period;
button.classList.toggle("on", on);
button.setAttribute("aria-pressed", on ? "true" : "false");
});
swappable.forEach(function (el) {
el.hidden = el.getAttribute("data-when") !== period;
});
}
buttons.forEach(function (button) {
button.addEventListener("click", function () {
show(button.getAttribute("data-period"));
});
});
});
// -------------------------------------------------------------------------
// Screenshot and video slots
//
// Every slot on this page already renders something real. This looks for a
// file named after the slot and, only if it loads, puts it on top. So the
// page is complete with an empty media/ folder, and adding art is dropping a
// file in — no markup to edit. site/media/README.md has the list.
// -------------------------------------------------------------------------
$$("[data-shot]").forEach(function (slot) {
var name = slot.getAttribute("data-shot");
var kind = slot.getAttribute("data-media") || "image";
if (kind === "video") {
var video = document.createElement("video");
video.muted = true;
video.loop = true;
video.playsInline = true;
video.autoplay = !calm;
video.controls = calm;
video.preload = "metadata";
video.src = "media/" + name + ".mp4";
video.addEventListener("loadeddata", function () {
slot.replaceChildren(video);
});
return;
}
var image = new Image();
image.decoding = "async";
image.loading = "lazy";
image.alt = slot.getAttribute("data-alt") || "";
image.addEventListener("load", function () {
if (image.naturalWidth < 2) return;
image.srcset = "media/" + name + ".png 1x, media/" + name + "@2x.png 2x";
slot.replaceChildren(image);
});
image.src = "media/" + name + ".png";
});
// Add ?slots to the URL to see where art goes and what each file is called.
if (/[?&]slots\b/.test(location.search)) document.body.classList.add("show-slots");
// -------------------------------------------------------------------------
// The one live number on the page, in the footer beside the GitHub link. It
// ships hidden and only appears if the count actually arrives, so a rate
// limit or an offline visitor sees a plain link rather than an empty chip.
// -------------------------------------------------------------------------
var stars = $("#stars");
if (stars) {
fetch("https://api.github.com/repos/shifulaboratories/Hired")
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (repo) {
if (!repo || typeof repo.stargazers_count !== "number") return;
stars.textContent =
repo.stargazers_count >= 1000
? (repo.stargazers_count / 1000).toFixed(1) + "k"
: String(repo.stargazers_count);
stars.hidden = false;
})
.catch(function () {});
}
})();