I design digital products, explore emerging tech and build things that live on the internet.
+
+
+
@@ -975,19 +1029,18 @@ const stickers = [
}
.env-cancel text {
- /* Revealed only once the fonts have settled; see below. */
- opacity: 0;
+ /* Painted straight away in the fallback face rather than held back until
+ the webfont lands: the stamp is the first thing on the page and a blank
+ ring for the length of a font request read as broken. Text on a path
+ re-measures when Hanken swaps in, so the legend does shift slightly on
+ the arc — but it is centred on the path at both sizes (startOffset 50%
+ with text-anchor middle), so it re-centres rather than sliding off. */
fill: rgba(16, 40, 26, 0.5);
font-size: 9px;
letter-spacing: 1.4px;
font-family: var(--font-heading);
}
- :root.fonts-ready .env-cancel text {
- opacity: 1;
- transition: opacity 0.2s ease-out;
- }
-
.env-waves path {
fill: none;
stroke: rgba(16, 40, 26, 0.45);
@@ -1275,16 +1328,15 @@ const stickers = [
display: block;
width: calc(var(--art-w, 94px) * var(--sticker-face, 1));
height: auto;
- /* A real die cut has a hard white margin, not a halo. Blurred shadows give
- a glow; these are zero-blur copies offset in eight directions, which
- compound into a solid outline that still follows the artwork's silhouette
- rather than its bounding box. The last one is the lift off the page. */
- filter:
- drop-shadow(1.4px 0 0 #fff) drop-shadow(-1.4px 0 0 #fff)
- drop-shadow(0 1.4px 0 #fff) drop-shadow(0 -1.4px 0 #fff)
- drop-shadow(1px 1px 0 #fff) drop-shadow(-1px 1px 0 #fff)
- drop-shadow(1px -1px 0 #fff) drop-shadow(-1px -1px 0 #fff)
- drop-shadow(0 5px 10px rgba(16, 40, 26, 0.32));
+ /* A real die cut has a hard white margin, not a halo, and it follows the
+ artwork's silhouette rather than its bounding box. #die-cut does that in
+ a single pass: dilate the alpha, flood it white, lay the art back on top.
+ It replaced eight zero-blur drop-shadows stacked in one chain — each link
+ re-expands the filter region, and on mobile Chrome and WebKit that hit
+ the region cap and cropped the result to a rectangle, which is exactly
+ the bounding box the technique existed to avoid. One pass computes the
+ region once, and the filter declares it outright. */
+ filter: url(#die-cut) drop-shadow(0 5px 10px rgba(16, 40, 26, 0.32));
}
.sticker-tip {
@@ -1471,6 +1523,70 @@ const stickers = [
.lab-footer a:focus-visible,
.lab-footer button:focus-visible { outline: 2px solid #fff; outline-offset: 4px; }
+ /* The bar answers to the same fixed, uppercase Departure Mono as the footer;
+ only the ink differs, because the top of the stage is blush and the foot is
+ the green ground band. */
+ .lab-topbar {
+ position: fixed;
+ left: 0;
+ right: 0;
+ top: 0;
+ z-index: 30;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1.5rem;
+ padding: 0.85rem 1.25rem;
+ font-family: var(--font-display);
+ font-size: 0.6rem;
+ letter-spacing: 0.09em;
+ text-transform: uppercase;
+ /* Nothing to say until the card is open, and nothing to catch a drag with
+ either — the letters can be thrown up here. */
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.35s ease-out;
+ }
+
+ /* data-done lives on the canvas, which is inside .lab-stage — the bar, like
+ the hint and the footer, is a sibling of the stage rather than of the
+ canvas, so it cannot reach that attribute with a combinator. render()
+ mirrors the flag onto the root element instead. */
+ :root.card-open .lab-topbar {
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+ .lab-topbar-name,
+ .lab-topbar button {
+ font: inherit;
+ letter-spacing: inherit;
+ text-transform: inherit;
+ /* The same ink as a letter's own heading — the bar is uppercase Departure
+ Mono like those headings are, so anything lighter read as a different,
+ fainter class of type sitting above the collage. */
+ color: var(--head-ink);
+ }
+
+ .lab-topbar-name { text-decoration: none; }
+
+ .lab-topbar button {
+ background: none;
+ border: 0;
+ padding: 0;
+ cursor: pointer;
+ }
+
+ .lab-topbar-name:hover,
+ .lab-topbar button:hover { text-decoration: underline; }
+
+ .lab-topbar-name:focus-visible,
+ .lab-topbar button:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }
+
+ @media (prefers-reduced-motion: reduce) {
+ .lab-topbar { transition: none; }
+ }
+
.lab-modal {
position: fixed;
inset: 0;
@@ -1540,6 +1656,15 @@ const stickers = [
margin: 0 0 0.35rem;
}
+ .lab-modal-panel .lab-modal-role {
+ font-family: var(--font-display);
+ font-size: 0.62rem;
+ letter-spacing: 0.09em;
+ text-transform: uppercase;
+ color: rgba(16, 40, 26, 0.55);
+ margin: -0.15rem 0 0.6rem;
+ }
+
.lab-modal-panel section { margin-bottom: 1.25rem; }
.lab-modal-panel p { margin: 0 0 0.35rem; }
.lab-modal-panel a { color: var(--mark); }
@@ -1580,15 +1705,6 @@ const stickers = [
.lab-modal-close:hover { color: var(--ink); }
-
-