2026-05-15 09:20:43 -07:00
<!doctype html>
< html lang = "en" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< link rel = "icon" type = "image/x-icon" href = "/favicon.ico" >
< title > moving images | hello hello< / title >
< meta name = "description" content = "Lee Cattarin... on the internet!" >
< link rel = "alternate" href = "/feed.xml" type = "application/atom+xml" title = "hello hello" >
< meta property = "og:title" content = "moving images | hello hello" >
< meta property = "og:type" content = "website" >
< meta property = "og:description" content = "Lee Cattarin... on the internet!" >
< meta property = "og:site_name" content = "hello hello" >
< meta property = "og:image" content = "/img/cormorant.jpg" >
< meta property = "og:image:alt" content = "Image unrelated to post. A cormorant, a type of black waterfowl, poses with wings spread on a buoy in Puget Sound. Off to the left, another bird floats." >
< meta name = "generator" content = "Eleventy v3.1.2" >
2026-05-21 23:35:46 +00:00
< meta itemprop = "ai-content-declaration:version" content = "1.0.0" >
< meta itemprop = "ai-content-declaration:level" content = "none" >
2026-05-15 09:20:43 -07:00
< link rel = "preconnect" href = "https://fonts.googleapis.com" >
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin = "" >
< link href = "https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:ital,wght@0,200..800;1,200..800&family=Atkinson+Hyperlegible+Next:ital,wght@0,200..800;1,200..800&display=swap" rel = "stylesheet" >
< script src = "https://kit.fontawesome.com/884dded219.js" crossorigin = "anonymous" > < / script >
< style > . p o s t - m e t a d a t a {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: baseline;
margin: 1.5rem 0 .5rem;
}
.post-metadata p {
font-size: .9rem;
margin: 0;
}
.post-tags {
display: flex;
flex-flow: row wrap;
gap: .5rem;
list-style: none;
margin: 0;
}
.post-tags li {
margin: 0;
}
.post-tags li a {
text-decoration: none;
color: var(--color-teal);
padding: 0 .5rem;
border-radius: 1rem;
box-shadow: .15rem .15rem var(--color-shadow);
border: .08rem solid var(--color-teal);
line-height: 2;
/* Click animation handling */
position: relative;
top: 0;
left: 0;
transition: top .1s ease-in, left .1s ease-in;
}
.post-tags li a:focus-visible {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
@media (any-hover: hover) {
.post-tags li a:hover {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
}
@media (forced-colors: active) {
.post-tags li a:focus-visible {
outline-offset: .08rem;
outline: .08rem solid;
}
@media (any-hover: hover) {
.post-tags li a:hover {
outline-offset: .08rem;
outline: .08rem solid;
}
}
}
/* Click animation */
.post-tags li a:active {
top: .1rem;
left: .1rem;
box-shadow: .05rem .05rem var(--color-shadow);
}
/* Adapted from PrismJS 1.30.0 Tomorrow Night theme
https://prismjs.com/download
*/
code,
pre,
code[class*=language-],
pre[class*=language-] {
font-family: var(--font-family-code);
background-color: var(--color-bg-alt);
font-size: .9em;
text-shadow: 0 1px var(--color-shadow);
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre,
pre[class*=language-] {
margin: 1rem 0;
padding: 1rem;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
border-radius: .5rem;
overflow: auto;
}
:not(pre)>code,
:not(pre)>code[class*=language-] {
padding: .2rem;
border-radius: .25rem;
white-space: normal;
}
/* Selected text */
code ::-moz-selection,
code::-moz-selection,
pre ::-moz-selection,
pre::-moz-selection,
code[class*=language-] ::-moz-selection,
code[class*=language-]::-moz-selection,
pre[class*=language-] ::-moz-selection,
pre[class*=language-]::-moz-selection,
code ::selection,
code::selection,
pre ::selection,
pre::selection,
code[class*=language-] ::selection,
code[class*=language-]::selection,
pre[class*=language-] ::selection,
pre[class*=language-]::selection {
text-shadow: none;
background-color: var(--color-bg);
}
/* Syntax highlighting */
.token.namespace {
opacity: .7;
}
.token.bold,
.token.important {
font-weight:700
}
.token.italic {
font-style:italic
}
.token.block-comment,
.token.cdata,
.token.comment,
.token.doctype,
.token.prolog,
.token.punctuation {
color: var(--color-grey);
}
.token.attr-name,
.token.deleted,
.token.namespace,
.token.tag {
color: var(--color-red);
}
.token.boolean,
.token.function,
.token.number {
color: var(--color-orange);
}
.token.class-name,
.token.constant,
.token.property,
.token.symbol {
color: var(--color-yellow);
}
.token.attr-value,
.token.char,
.token.regex,
.token.string,
.token.variable,
.token.inserted {
color: var(--color-green);
}
.token.entity,
.token.operator,
.token.url,
.token.function-name {
color:var(--color-blue);
}
.token.atrule,
.token.builtin,
.token.important,
.token.keyword,
.token.selector {
color: var(--color-purple);
}
#postlist,
#taglist {
list-style: none;
}
#postlist, .post,
#taglist, .tag {
margin: 0;
}
/* Odd-numbered posts & tag layout/coloration */
.post:nth-child(odd) .postlink,
.tag:nth-child(odd) .taglink {
grid-template-areas:
'img h2'
'img info'
'img .';
grid-template-columns: 45% auto;;
--color-primary: var(--color-teal);
--color-accent: var(--color-pink);
}
/* Even-numbered posts & tags layout/coloration */
.post:nth-child(even) .postlink,
.tag:nth-child(even) .taglink {
grid-template-areas:
'h2 img'
'info img'
'. img';
grid-template-columns: auto 45%;
--color-primary: var(--color-pink);
--color-accent: var(--color-teal);
}
/* Layout for all posts on mobile */
@media (max-width: 650px) {
.post:nth-child(n) .postlink,
.tag:nth-child(n) .taglink {
grid-template-areas:
'img'
'h2'
'info';
grid-template-columns: auto;
}
}
/* Link */
.postlink,
.taglink {
display: grid;
border: .25rem solid var(--color-primary);
border-radius: 1.25rem;
box-shadow: .35rem .35rem var(--color-shadow);
margin: 2rem 0;
text-decoration: none;
/* Click animation handling */
position: relative;
top: 0;
left: 0;
transition: top .05s ease-in, left .05s ease-in;
}
.postlink:focus-visible,
.taglink:focus-visible {
background-color: var(--color-primary);
outline: none;
}
@media (any-hover: hover) {
.postlink:hover,
.taglink:hover {
background-color: var(--color-primary);
}
}
/* Forced colors */
@media (forced-colors: active) {
.postlink:focus-visible,
.taglink:focus-visible {
outline-offset: .25rem;
outline: .25rem solid;
}
@media (any-hover: hover) {
.postlink:hover,
.taglink:hover {
outline-offset: .25rem;
outline: .25rem solid;
}
}
}
/* Click animation */
.postlink:active,
.taglink:active {
box-shadow: none;
top: .2rem;
left: .2rem;
box-shadow: .15rem .15rem var(--color-shadow);
}
/* Post & tag elements */
.post h2, .post img,
.post ul, .post li,
.tag h2, .tag p,
.tag img {
margin: 0;
}
.post h2,
.tag h2 {
grid-area: h2;
padding: .25rem .5rem;
text-transform: uppercase;
font-size: 1.5rem;
color: var(--color-primary);
border-radius: 1rem 1rem 0 0;
border-bottom: .25rem solid var(--color-accent);
}
.post:nth-child(even) h2,
.tag:nth-child(even) h2 {
text-align: right;
}
.postlink:focus-visible h2,
.taglink:focus-visible h2 {
color: var(--color-bg);
border-color: var(--color-bg);
}
@media (any-hover: hover) {
.postlink:hover h2,
.taglink:hover h2 {
color: var(--color-bg);
border-color: var(--color-bg);
}
}
/* Images */
.post img,
.tag-imgs {
grid-area: img;
}
.blur {
filter: blur(.75rem);
clip-path: inset(0 round 1rem);
}
.tag-imgs {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: .15rem;
}
.tag-imgs img {
aspect-ratio: 3 / 2;
object-fit: cover;
}
.missing-image {
width: 100%;
aspect-ratio: 3 / 2;
background-color: var(--color-bg-alt);
border-radius: 1rem;
}
.taglink:focus-visible .missing-image {
opacity: .7;
}
@media (any-hover: hover) {
.taglink:hover .missing-image {
opacity: .7;
}
}
/* Post tags */
.postlist-tags {
grid-area: info;
list-style: none;
display: flex;
flex-flow: row wrap;
gap: .5rem;
padding: .5rem;
}
.post:nth-child(odd) .postlist-tags {
justify-content: flex-end;
}
.postlist-tags li,
.tagcount {
background-color: var(--color-primary);
color: var(--color-bg);
padding: 0 .5rem;
border-radius: 1rem;
}
.postlink:focus-visible .postlist-tags li,
.taglink:focus-visible .tagcount {
background-color: var(--color-bg);
color: var(--color-primary);
}
@media (any-hover: hover) {
.postlink:hover .postlist-tags li,
.taglink:hover .tagcount {
background-color: var(--color-bg);
color: var(--color-primary);
}
}
/* Tag count */
.tag p {
grid-area: info;
padding: .5rem;
}
.tag:nth-child(odd) p {
text-align: right;
}
img[tabindex="0"] {
cursor: pointer;
}
img[tabindex="0"]:focus-visible {
outline: .15rem solid var(--color-teal);
}
dialog {
margin: auto;
flex-flow: column;
background: transparent;
border: none;
position: absolute;
}
dialog[open] {
display: flex;
}
dialog::backdrop {
background-color: rgba(from var(--color-bg) r g b / .8);
backdrop-filter: blur(4px);
}
body:has(dialog[open]) {
overflow: hidden;
}
.close-dialog {
background-color: rgba(from var(--color-teal) r g b / .35);
font-size: 1.5rem;
padding: 0 .5rem .15rem;
border-radius: 1rem;
color: var(--color-teal);
box-shadow: .15rem .15rem var(--color-shadow);
border: .08rem solid var(--color-teal);
align-self: flex-end;
justify-self: flex-start;
margin: 0 .1rem;
/* Click animation handling */
position: relative;
top: 1rem;
left: -.15rem;
transition: top .1s ease-in, left .1s ease-in;
}
.close-dialog:focus-visible {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
@media (any-hover: hover) {
.close-dialog:hover {
outline: none;
background-color: var(--color-teal);
color: var(--color-bg);
}
}
@media (forced-colors: active) {
.close-dialog:focus-visible {
outline-offset: .08rem;
outline: .08rem solid;
}
@media (any-hover: hover) {
.close-dialog:hover {
outline-offset: .08rem;
outline: .08rem solid;
}
}
}
/* Click animation */
.close-dialog:active {
top: 1.1rem;
left: -.05rem;
box-shadow: .05rem .05rem var(--color-shadow);
}
.modal-img {
max-height: calc(90vh - 2rem);
object-fit: contain;
width: auto;
max-width: 90%;
margin: 0 auto;
}
:root {
color-scheme: light dark;
--font-family: 'Atkinson Hyperlegible Next', sans-serif;
--font-family-code: 'Atkinson Hyperlegible Mono', monospace;
--color-dark: #2e303e;
--color-dark-alt: #3c3f52;
--color-light: #ebeeef;
--color-light-alt: #dbe1e3;
--color-teal-dark: #18737b;
--color-teal-light: #25b0bc;
--color-pink-dark: #94195d;
--color-pink-light: #ee9fcb;
--color-shadow: rgba(2, 10, 40, .25);
/* Used for syntax highlighting */
--color-red-light: #f195aa;
--color-orange-light: #fab795;
--color-yellow-light: #fbe6bc;
--color-green-light: #29d398;
--color-blue-light: #26bbd9;
--color-purple-light: #ddaeea;
--color-grey-light: #b9c3c6;
--color-red-dark: #991433;
--color-orange-dark: #883206;
--color-yellow-dark: #6a4906;
--color-green-dark: #125940;
--color-blue-dark: #125663;
--color-purple-dark: #722999;
--color-grey-dark: #4a4b64;
--color-text: light-dark(var(--color-dark), var(--color-light));
--color-bg: light-dark(var(--color-light), var(--color-dark));
--color-text-alt: light-dark(var(--color-dark-alt), var(--color-light-alt));
--color-bg-alt: light-dark(var(--color-light-alt), var(--color-dark-alt));
--color-teal: light-dark(var(--color-teal-dark), var(--color-teal-light));
--color-pink: light-dark(var(--color-pink-dark), var(--color-pink-light));
--color-red: light-dark(var(--color-red-dark), var(--color-red-light));
--color-orange: light-dark(var(--color-orange-dark), var(--color-orange-light));
--color-yellow: light-dark(var(--color-yellow-dark), var(--color-yellow-light));
--color-green: light-dark(var(--color-green-dark), var(--color-green-light));
--color-blue: light-dark(var(--color-blue-dark), var(--color-blue-light));
--color-purple: light-dark(var(--color-purple-dark), var(--color-purple-light));
--color-grey: light-dark(var(--color-grey-dark), var(--color-grey-light));
}
/* Base */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-family);
color: var(--color-text);
background-color: var(--color-bg);
}
main {
width: 60vw;
max-width: 1000px;
margin: 0 auto;
scroll-margin-top: 7rem;
}
@media (max-width: 1050px) {
main {
width: 75vw;
}
}
@media (max-width: 650px) {
main {
width: 92vw;
}
}
/* Headers */
h1, h2, h3, h4, h5, h6 {
line-height: 1.25;
color: var(--color-teal);
}
h1 {
margin-top: 3rem;
font-size: 3.5rem;
text-align: center;
}
h2, h3, h4, h5, h6 {
scroll-margin-top: 5rem;
}
h2 {
margin-top: 2rem;
font-size: 2.2rem;
}
h3 {
margin-top: 1.5rem;
font-size: 1.6rem;
}
@media (max-width: 650px) {
h1 { font-size: 2.8rem; }
h2 { font-size: 1.8rem; }
h3 { font-size: 1.35rem; }
}
h4, h5, h6 {
margin-top: 1rem;
font-size: 1.2rem;
}
/* Images */
img {
display: block;
max-width: 100%;
height: auto;
border-radius: 1rem;
margin: 0 auto;
}
/* Paragraphs */
p {
margin: 1.25rem 0;
line-height: 1.4;
}
strong,
b {
font-weight: 900;
}
/* Links */
a {
color: var(--color-text);
border-radius: .25rem;
text-decoration: underline;
text-decoration-style: solid;
text-decoration-thickness: .2em;
text-decoration-color: var(--color-teal);
transition: text-decoration-thickness .5s;
}
a:focus-visible {
text-decoration: none;
outline: .15rem solid var(--color-teal);
}
@media (any-hover: hover) {
a:hover {
text-decoration-thickness: .4em;
}
}
a:active {
text-decoration-thickness: .4em;
}
/* Heading anchors */
a.ha,
span.ha-placeholder {
color: var(--color-pink);
font-size: .8em;
vertical-align: .1em;
}
span.ha-placeholder {
opacity: .55;
}
/* Lists */
::marker {
color: var(--color-pink);
}
ul, ol, dl, li {
margin-left: 1rem;
}
@media (max-width: 650px) {
ul, ol, dl {
margin-left: 0;
}
}
li {
line-height: 1.2 5;
margin-top: .65rem;
margin-bottom: .65rem;
}
li ul, li ol {
margin: .5rem 0;
}
dt {
font-weight: 900;
margin-top: .5rem;
}
dd {
margin-left: 2rem;
margin-bottom: .75rem;
}
/* Blockquotes */
blockquote {
margin: .5rem 0;
padding: 0 1rem;
border-radius: .25rem 1rem 1rem .25rem;
line-height: 1.25;
border-left: .5rem solid var(--color-pink);
}
blockquote,
blockquote p,
blockquote ol,
blockquote ul {
background-color: var(--color-bg-alt);
padding: .5rem;
}
blockquote p {
margin: 0;
}
/* Tables */
table {
width: 100%;
border-spacing: 0; /* border collapse doesn't play nice with radii */
border-radius: .3rem;
border: thin solid var(--color-pink);
}
th {
color: var(--color-bg);
background-color: var(--color-pink);
}
th code {
color: var(--color-text); /* Yes, I actually do this somewhere */
}
th, td {
padding: .5rem;
text-align: left;
}
tr:nth-child(even) { background-color: var(--color-bg-alt); }
th:not(:first-child) { border-left: thin solid var(--color-bg); }
th:first-child { border-top-left-radius: .25rem; }
th:last-child { border-top-right-radius: .25rem; }
td:not(:first-child) { border-left: thin solid var(--color-pink); }
/* Times */
time {
color: var(--color-grey);
}
/* Horizontal rules */
hr {
border: .25rem solid var(--color-pink);
margin: 2rem 0;
}
/* Used on home, reference, gallery pages */
.centered {
text-align: center;
}
/* Currently only used for resume, but it's generalizable */
.upper {
text-transform: uppercase;
}
/* Header */
header {
position: sticky;
top: 0;
background-color: var(--color-bg);
padding: .75rem 0;
z-index: 10;
border-bottom: thick solid var(--color-teal);
box-shadow: 0 .25rem .15rem var(--color-shadow);
}
/* Header links, pagination links */
header a,
.pagination a,
.webring ul a {
border-radius: 1rem;
border: .125rem solid var(--color-pink);
color: var(--color-pink);
text-decoration: none;
padding: 0 .25rem;
box-shadow: .15rem .15rem var(--color-shadow);
font-size: 1.2rem;
/* Click animation handling */
position: relative;
top: 0;
left: 0;
transition: top .05s ease-in, left .05s ease-in;
}
header a,
.pagination .older a,
.webring .prev a,
.webring .rand a {
padding-right: .35rem;
}
.pagination .newer a,
.webring .next a,
.webring .rand a {
padding-left: .35rem;
}
header a:focus-visible,
.pagination a:focus-visible,
.webring ul a:focus-visible {
color: var(--color-bg);
border-color: var(--color-pink);
background-color: var(--color-pink);
outline: none;
}
@media (any-hover: hover) {
header a:hover,
.pagination a:hover,
.webring ul a:hover {
color: var(--color-bg);
border-color: var(--color-pink);
background-color: var(--color-pink);
}
}
@media (forced-colors: active) {
header a:focus-visible,
.pagination a:focus-visible,
.webring ul a:focus-visible {
outline-offset: .125rem;
outline: .125rem solid;
}
@media (any-hover: hover) {
header a:hover,
.pagination a:hover,
.webring ul a:hover {
outline-offset: .125rem;
outline: .125rem solid;
}
}
}
/* Click animation */
header a:active,
.pagination a:active,
.webring ul a:active {
top: .1rem;
left: .1rem;
box-shadow: .05rem .05rem var(--color-shadow);
}
/* Current page */
header a[aria-current="page"] {
border-color: var(--color-teal);
color: var(--color-teal);
}
header a[aria-current="page"]:focus-visible {
color: var(--color-bg);
border-color: var(--color-teal);
background-color: var(--color-teal);
}
@media (any-hover: hover) {
header a[aria-current="page"]:hover {
color: var(--color-bg);
background-color: var(--color-teal);
border-color: var(--color-teal);
}
}
/* Header link icons, pagination icons */
header i,
.pagination i,
.webring ul i {
color: var(--color-teal);
}
header i,
.pagination .older i,
.webring .prev i,
.webring .rand i:nth-child(1) {
padding-left: .25rem;
}
.pagination .newer i,
.webring .next i,
.webring .rand i:nth-child(2) {
padding-right: .25rem;
}
header a[aria-current="page"] i {
color: var(--color-pink);
}
header a:focus-visible i,
a[aria-current="page"] a:focus-visible i,
.pagination a:focus-visible i,
.webring ul a:focus-visible i {
color: var(--color-bg);
}
@media (any-hover: hover) {
header a:hover i,
header a[aria-current="page"]:hover i,
.pagination a:hover i,
.webring ul a:hover i {
color: var(--color-bg);
}
}
/* Skip link */
#skip {
left: -999px;
position: absolute;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
z-index: -99;
}
#skip:focus-visible {
display: inline-block;
left: auto;
top: auto;
width: auto;
height: auto;
overflow: auto;
margin: 0 10%;
z-index: 999;
}
/* Nav */
header ul {
display: flex;
list-style: none;
gap: 1rem;
justify-content: center;
}
header ul,
header li {
margin: 0;
}
@media (max-width: 650px) {
.menu-text {
display: none; /* Icons only on small screens */
}
header a {
padding: .15rem .5rem;
}
header i {
padding: 0;
}
}
/* Footer */
footer {
padding: 1rem 0;
font-size: .9rem;
border-top: thick solid var(--color-pink);
margin-top: 2rem;
}
footer ul {
display: flex;
list-style: none;
gap: .5rem;
justify-content: center;
margin: 0;
}
footer li {
margin: 0;
}
footer li:nth-child(2)::before,
footer li:nth-child(2)::after {
content: " ● " / "";
color: var(--color-teal);
}
@media (max-width: 650px) {
footer ul {
flex-flow: column;
text-align: center;
}
footer li:nth-child(2)::before,
footer li:nth-child(2)::after {
content: none;
}
}
footer a {
text-decoration-color: var(--color-pink);
}
footer a:focus-visible {
outline-color: var(--color-pink);
}
/* Pagination */
.pagination,
.pagination li {
margin: 0;
}
.pagination {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-areas: "older newer";
list-style: none;
margin-top: 3rem;
}
@media (max-width: 650px) {
.post-pagination {
grid-template-columns: 1fr;
grid-template-areas:
"older"
"newer";
gap: .75rem;
}
}
.pagination .older {
grid-area: older;
}
.pagination .newer {
grid-area: newer;
text-align: right;
}
/* webring navigation */
.webring {
margin-bottom: 3rem;
}
.webring ul {
display: flex;
flex-flow: row wrap;
list-style: none;
justify-content: space-around;
gap: .35rem;
}
.webring ul,
.webring li {
margin-left: 0;
}
@media print {
/* Nav elements */
header,
footer,
nav {
display: none !important;
}
/* Base */
body {
background-color: #fff;
color: #000;
}
main {
width: 95vw;
}
h1,h2,h3,h4,h5,h6 {
color: #000;
}
/* Links */
/* Hover is not really necessary, but it's annoying when testing otherwise */
a,
a:hover {
text-decoration-style: dotted;
text-decoration-thickness: .1rem;
text-decoration-color: #000;
}
a::after{
content: " (" attr(href) ")";
}
/* Code */
code,
pre,
code[class*=language-],
pre[class*=language-] {
text-shadow: none;
background-color: var(--color-light);
color: #000 !important;
}
.token.namespace {
opacity: 1;
}
.token.block-comment,
.token.cdata,
.token.comment,
.token.doctype,
.token.prolog,
.token.punctuation,
.token.attr-name,
.token.deleted,
.token.namespace,
.token.tag,
.token.boolean,
.token.function,
.token.number,
.token.class-name,
.token.constant,
.token.property,
.token.symbol,
.token.attr-value,
.token.char,
.token.regex,
.token.string,
.token.variable,
.token.inserted,
.token.entity,
.token.operator,
.token.url,
.token.function-name,
.token.atrule,
.token.builtin,
.token.important,
.token.keyword,
.token.selector {
color: #000;
}
}< / style >
< script type = "module" > / / T h a n k y o u t o h t t p s : / / g i t h u b . c o m / d a v i d d a r n e s / h e a d i n g - a n c h o r s
// Thank you to https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
let globalInstanceIndex = 0;
class HeadingAnchors extends HTMLElement {
static register(tagName = "heading-anchors", registry = window.customElements) {
if(registry & & !registry.get(tagName)) {
registry.define(tagName, this);
}
}
static attributes = {
exclude: "data-ha-exclude",
prefix: "prefix",
content: "content",
}
static classes = {
anchor: "ha",
placeholder: "ha-placeholder",
srOnly: "ha-visualhide",
}
static defaultSelector = "h2,h3,h4,h5,h6";
static css = `
.${HeadingAnchors.classes.srOnly} {
clip: rect(0 0 0 0);
height: 1px;
overflow: hidden;
position: absolute;
width: 1px;
}
.${HeadingAnchors.classes.anchor} {
position: absolute;
left: var(--ha_offsetx);
top: var(--ha_offsety);
text-decoration: none;
opacity: 0;
}
.${HeadingAnchors.classes.placeholder} {
opacity: .3;
}
.${HeadingAnchors.classes.anchor}:is(:focus-within, :hover) {
opacity: 1;
}
.${HeadingAnchors.classes.anchor},
.${HeadingAnchors.classes.placeholder} {
display: inline-block;
padding: 0 .25em;
/* Disable selection of visually hidden label */
-webkit-user-select: none;
user-select: none;
}
@supports (anchor-name: none) {
.${HeadingAnchors.classes.anchor} {
position: absolute;
left: anchor(left);
top: anchor(top);
}
}`;
get supports() {
return "replaceSync" in CSSStyleSheet.prototype;
}
get supportsAnchorPosition() {
return CSS.supports("anchor-name: none");
}
constructor() {
super();
if(!this.supports) {
return;
}
let sheet = new CSSStyleSheet();
sheet.replaceSync(HeadingAnchors.css);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
this.headingStyles = {};
this.instanceIndex = globalInstanceIndex++;
}
connectedCallback() {
if (!this.supports) {
return;
}
this.headings.forEach((heading, index) => {
if(!heading.hasAttribute(HeadingAnchors.attributes.exclude)) {
let anchor = this.getAnchorElement(heading);
let placeholder = this.getPlaceholderElement();
// Prefers anchor position approach for better accessibility
// https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
if(this.supportsAnchorPosition) {
let anchorName = `--ha_${this.instanceIndex}_${index}`;
placeholder.style.setProperty("anchor-name", anchorName);
anchor.style.positionAnchor = anchorName;
}
heading.appendChild(placeholder);
heading.after(anchor);
}
});
}
// Polyfill-only
positionAnchorFromPlaceholder(placeholder) {
if(!placeholder) {
return;
}
let heading = placeholder.closest("h1,h2,h3,h4,h5,h6");
if(!heading.nextElementSibling) {
return;
}
// TODO next element could be more defensive
this.positionAnchor(heading.nextElementSibling);
}
// Polyfill-only
positionAnchor(anchor) {
if(!anchor || !anchor.previousElementSibling) {
return;
}
// TODO previous element could be more defensive
let heading = anchor.previousElementSibling;
this.setFontProp(heading, anchor);
if(this.supportsAnchorPosition) {
// quit early
return;
}
let placeholder = heading.querySelector(`.${HeadingAnchors.classes.placeholder}`);
if(placeholder) {
anchor.style.setProperty("--ha_offsetx", `${placeholder.offsetLeft}px`);
anchor.style.setProperty("--ha_offsety", `${placeholder.offsetTop}px`);
}
}
setFontProp(heading, anchor) {
let placeholder = heading.querySelector(`.${HeadingAnchors.classes.placeholder}`);
if(placeholder) {
let style = getComputedStyle(placeholder);
let props = ["font-weight", "font-size", "line-height", "font-family"];
let [weight, size, lh, family] = props.map(name => style.getPropertyValue(name));
anchor.style.setProperty("font", `${weight} ${size}/${lh} ${family}`);
let vars = style.getPropertyValue("font-variation-settings");
if(vars) {
anchor.style.setProperty("font-variation-settings", vars);
}
}
}
/* no longer used, using aria-labelledby instead - see getAnchorElement
getAccessibleTextPrefix() {
// Useful for i18n
return this.getAttribute(HeadingAnchors.attributes.prefix) || "Jump to section titled";
}
*/
getContent() {
if(this.hasAttribute(HeadingAnchors.attributes.content)) {
return this.getAttribute(HeadingAnchors.attributes.content);
}
return "#";
}
// Placeholder nests inside of heading
getPlaceholderElement() {
let ph = document.createElement("span");
ph.setAttribute("aria-hidden", true);
ph.classList.add(HeadingAnchors.classes.placeholder);
let content = this.getContent();
if(content) {
ph.innerHTML = content; // CHANGED HERE
}
ph.addEventListener("mouseover", (e) => {
let placeholder = e.target.closest(`.${HeadingAnchors.classes.placeholder}`);
if(placeholder) {
this.positionAnchorFromPlaceholder(placeholder);
}
});
return ph;
}
getAnchorElement(heading) {
let anchor = document.createElement("a");
anchor.href = `#${heading.id}`;
anchor.classList.add(HeadingAnchors.classes.anchor);
let content = this.getContent();
anchor.innerHTML = `< span aria-labelledby = ${heading.id} > ${content}< / span > `; // CHANGED HERE
anchor.addEventListener("focus", e => {
let anchor = e.target.closest(`.${HeadingAnchors.classes.anchor}`);
if(anchor) {
this.positionAnchor(anchor);
}
});
anchor.addEventListener("mouseover", (e) => {
// when CSS anchor positioning is supported, this is only used to set the font
let anchor = e.target.closest(`.${HeadingAnchors.classes.anchor}`);
this.positionAnchor(anchor);
});
return anchor;
}
get headings() {
return this.querySelectorAll(this.selector.split(",").map(entry => `${entry.trim()}[id]`));
}
get selector() {
return this.getAttribute("selector") || HeadingAnchors.defaultSelector;
}
}
HeadingAnchors.register();
export { HeadingAnchors }
/* don't even bother on mobile */
if (window.innerWidth > 650) {
const dialog = document.querySelector("dialog");
const closeButton = document.querySelector(".close-dialog");
const hero = document.querySelector(".hero");
hero.tabIndex = 0;
hero.addEventListener("click", (e) => dialog.showModal());
hero.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
dialog.showModal();
}
});
closeButton.addEventListener("click", (e) => dialog.close());
closeButton.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
dialog.close();
}
});
}< / script >
< / head >
< body >
< header >
< a href = "#main" id = "skip" title = "skip to main content" >
< i class = "fa-solid fa-forward" aria-hidden = "true" > < / i > skip
< / a >
< nav aria-label = "main navigation" >
< ul >
< li >
< a href = "/reference/" title = "read reference posts" >
< i class = "fa-regular fa-folder-open" aria-hidden = "true" > < / i >
< span class = "menu-text" > reference< / span >
< / a >
< / li >
< li >
< a href = "/gallery/" title = "view the gallery" >
< i class = "fa-regular fa-images" aria-hidden = "true" > < / i >
< span class = "menu-text" > gallery< / span >
< / a >
< / li >
< li >
< a href = "/" title = "home" >
< i class = "fa fa-solid fa-crow" aria-hidden = "true" > < / i >
< span class = "menu-text" > home< / span >
< / a >
< / li >
< li >
< a href = "/about/" title = "about Lee" >
< i class = "fa-regular fa-user" aria-hidden = "true" > < / i >
< span class = "menu-text" > about< / span >
< / a >
< / li >
< li >
< a href = "/contact/" title = "contact Lee" >
< i class = "fa-solid fa-envelope-open-text" aria-hidden = "true" > < / i >
< span class = "menu-text" > contact< / span >
< / a >
< / li >
< / ul >
< / nav >
< / header >
< main id = "main" >
< heading-anchors content = "<i class='fa-solid fa-anchor'></i>" >
< article >
< h1 id = "moving-images" > moving images< / h1 >
< div class = "post-metadata" >
< p >
posted on
< time datetime = "2026-01-05" > January 5, 2026< / time >
by Lee Cattarin
< / p >
< ul class = "post-tags" >
< li >
< a href = "/tags/software/" > software< / a >
< / li >
< / ul >
< / div >
< dialog closedby = "any" aria-label = "image modal" tabindex = "-1" >
< button class = "close-dialog" autofocus = "" aria-label = "close the image modal" > × < / button >
< img class = "modal-img" src = "/img/cormorant.jpg" alt = "Image unrelated to post. A cormorant, a type of black waterfowl, poses with wings spread on a buoy in Puget Sound. Off to the left, another bird floats." loading = "lazy" decoding = "async" width = "1000" height = "666" >
< / dialog >
< img class = "hero" src = "/img/cormorant.jpg" alt = "Image unrelated to post. A cormorant, a type of black waterfowl, poses with wings spread on a buoy in Puget Sound. Off to the left, another bird floats." loading = "lazy" decoding = "async" width = "1000" height = "666" >
< h2 id = "problem-statement" > problem statement< / h2 >
< p > today I decided to finally clean up the < code > assets/img< / code > directory for this site. Since 2022, when I started this project, I've just been adding images directly to that directory with no further segmentation - messy of me, I know! It's gotten unwieldy and I'm starting to get worried about generic names leading to duplicates at some point, particularly for the non-gallery images where I have a tendency to < a href = "/stationery-exchange" > use< / a > < a href = "/favorite-git-flag" > lots< / a > < a href = "/trans-networks" > of< / a > < a href = "/no-politics" > mushroom< / a > < a href = "/domain-and-site-setup" > images< / a > .< / p >
< p > so it's time to move them into year-based folders. Let's talk about how I did that. < code > bash< / code > away!< / p >
< p > (want to < a href = "#result" > skip right to the completed script?< / a > )< / p >
< h2 id = "find" > < code > find< / code > < / h2 >
< p > let's start with the basics: a list of posts. < code > find< / code > gets us everything under a specific directory - in this case, the < code > _posts< / code > directory. We can filter out the directories a few different ways, but I piped the < code > find< / code > output through a basic < code > grep< / code > looking for < code > .md< / code > in the filename.< / p >
< pre class = "language-sh" > < code class = "language-sh" > < span class = "token keyword" > for< / span > < span class = "token for-or-select variable" > FILE< / span > < span class = "token keyword" > in< / span > < span class = "token variable" > < span class = "token variable" > $(< / span > < span class = "token function" > find< / span > _posts < span class = "token operator" > |< / span > < span class = "token function" > grep< / span > .md< span class = "token variable" > )< / span > < / span >
< span class = "token keyword" > do< / span >
< span class = "token comment" > # TBD< / span >
< span class = "token keyword" > done< / span > < / code > < / pre >
< h2 id = "grep" > < code > grep< / code > < / h2 >
< p > < code > grep< / code > can also help us get image names with the regex < code > " name:.+jpg|png" < / code > . I add < code > name:< / code > to the regex because there are < em > very occasionally< / em > images that aren't the featured image for the post, and those don't fit the pattern of < code > name: < img> < / code > . Since there's so few of those, I ended up handling them manually.< / p >
< p > to make < code > grep< / code > work with regex, it needs the < code > -E< / code > flag.< / p >
< pre class = "language-sh" > < code class = "language-sh" > < span class = "token comment" > # gives us< / span >
< span class = "token comment" > # name: < img>< / span >
< span class = "token comment" > # note the 4 spaces at the beginning of the line< / span >
< span class = "token assign-left variable" > IMAGE_LINE< / span > < span class = "token operator" > =< / span > < span class = "token variable" > < span class = "token variable" > $(< / span > < span class = "token function" > cat< / span > $FILE < span class = "token operator" > |< / span > < span class = "token function" > grep< / span > < span class = "token parameter variable" > -E< / span > < span class = "token string" > "name:.+jpg|png$"< / span > < span class = "token variable" > )< / span > < / span > < / code > < / pre >
< h2 id = "cut" > < code > cut< / code > < / h2 >
< p > that output gets us the full line of text that includes the image filename. Let's trim out what we actually want.< / p >
< p > below, < code > -d< / code > sets a delimiter, and < code > -f< / code > chooses what field we want to return. Because there's 4 spaces before < code > name< / code > , our field index is actually pretty high - < code > cut< / code > is creating 4 empty strings.< / p >
< pre class = "language-sh" > < code class = "language-sh" > < span class = "token assign-left variable" > IMAGE< / span > < span class = "token operator" > =< / span > < span class = "token variable" > < span class = "token variable" > $(< / span > < span class = "token builtin class-name" > echo< / span > $IMAGE_LINE < span class = "token operator" > |< / span > < span class = "token function" > cut< / span > < span class = "token parameter variable" > -d< / span > < span class = "token string" > ' '< / span > < span class = "token parameter variable" > -f< / span > < span class = "token number" > 6< / span > -< span class = "token variable" > )< / span > < / span > < / code > < / pre >
< p > or, for brevity:< / p >
< pre class = "language-sh" > < code class = "language-sh" > < span class = "token assign-left variable" > IMAGE< / span > < span class = "token operator" > =< / span > < span class = "token variable" > < span class = "token variable" > $(< / span > < span class = "token function" > cat< / span > $FILE < span class = "token operator" > |< / span > < span class = "token function" > grep< / span > < span class = "token parameter variable" > -E< / span > < span class = "token string" > "name:.+jpg|png$"< / span > < span class = "token operator" > |< / span > < span class = "token function" > cut< / span > < span class = "token parameter variable" > -d< / span > < span class = "token string" > ' '< / span > < span class = "token parameter variable" > -f< / span > < span class = "token number" > 6< / span > -< span class = "token variable" > )< / span > < / span > < / code > < / pre >
< p > with < code > cut< / code > , we can also get the year of the post:< / p >
< pre class = "language-sh" > < code class = "language-sh" > < span class = "token assign-left variable" > YEAR< / span > < span class = "token operator" > =< / span > < span class = "token variable" > < span class = "token variable" > $(< / span > < span class = "token builtin class-name" > echo< / span > $FILE < span class = "token operator" > |< / span > < span class = "token function" > cut< / span > < span class = "token parameter variable" > -d< / span > < span class = "token string" > '/'< / span > < span class = "token parameter variable" > -f< / span > < span class = "token number" > 2< / span > -< span class = "token variable" > )< / span > < / span > < / code > < / pre >
< h2 id = "sed" > < code > sed< / code > < / h2 >
< p > there's two major things we need to do with the information we've gathered:< / p >
< ol >
< li > replace the image filename in-place in the post's markdown file< / li >
< li > move the image file from its original location into a new directory< / li >
< / ol >
< p > we can do replacement with < code > sed< / code > , where our pattern should be something like this: < code > s/$IMAGE/$YEAR\/& \< / code > (the < code > & < / code > subs in the found string - in this case < code > $IMAGE< / code > ). We could also use comma separators if we don't want to escape the slash, like < code > s,$IMAGE,$YEAR/& ,< / code > - I did this for ease of reading.< / p >
< p > by default, < code > sed< / code > prints to standard output, so we'll tell it to edit in-place instead with < code > -i< / code > . Here's our full < code > sed< / code > command:< / p >
< pre class = "language-sh" > < code class = "language-sh" > < span class = "token function" > sed< / span > < span class = "token string" > "s,< span class = "token variable" > $IMAGE< / span > ,< span class = "token variable" > $YEAR< / span > /& ,"< / span > < span class = "token parameter variable" > -i< / span > < span class = "token variable" > $FILE< / span > < / code > < / pre >
< h2 id = "mving-and-shaking" > < code > mv< / code > ing and shaking< / h2 >
< p > (my mom thinks I'm funny.)< / p >
< p > now we'll handle moving the image file from its original location into a new directory. let's create our image paths, source and destination:< / p >
< pre class = "language-sh" > < code class = "language-sh" > < span class = "token assign-left variable" > IMG_DIR< / span > < span class = "token operator" > =< / span > assets/img
< span class = "token assign-left variable" > NEW_IMAGE< / span > < span class = "token operator" > =< / span > < span class = "token variable" > $IMG_DIR< / span > /< span class = "token variable" > $YEAR< / span > /< span class = "token variable" > $IMAGE< / span >
< span class = "token assign-left variable" > IMAGE< / span > < span class = "token operator" > =< / span > < span class = "token variable" > $IMG_DIR< / span > /< span class = "token variable" > $IMAGE< / span > < / code > < / pre >
< p > trying to < code > mv< / code > the images will immediately cause problems, because the year directories don't exist yet. A simple check gets us past that:< / p >
< pre class = "language-sh" > < code class = "language-sh" > < span class = "token keyword" > if< / span > < span class = "token punctuation" > [< / span > < span class = "token operator" > !< / span > < span class = "token parameter variable" > -d< / span > < span class = "token variable" > $IMG_DIR< / span > /< span class = "token variable" > $YEAR< / span > < span class = "token punctuation" > ]< / span >
< span class = "token keyword" > then< / span >
< span class = "token function" > mkdir< / span > < span class = "token variable" > $IMG_DIR< / span > /< span class = "token variable" > $YEAR< / span >
< span class = "token keyword" > fi< / span > < / code > < / pre >
< p > finally, we can < code > mv< / code > the image:< / p >
< pre class = "language-sh" > < code class = "language-sh" > < span class = "token function" > mv< / span > < span class = "token variable" > $IMAGE< / span > < span class = "token variable" > $NEW_IMAGE< / span > < / code > < / pre >
< h2 id = "result" > result< / h2 >
< p > here's our final script:< / p >
< pre class = "language-sh" > < code class = "language-sh" > < span class = "token keyword" > for< / span > < span class = "token for-or-select variable" > FILE< / span > < span class = "token keyword" > in< / span > < span class = "token variable" > < span class = "token variable" > $(< / span > < span class = "token function" > find< / span > _posts < span class = "token operator" > |< / span > < span class = "token function" > grep< / span > .md< span class = "token variable" > )< / span > < / span >
< span class = "token keyword" > do< / span >
< span class = "token comment" > # parse image and year info< / span >
< span class = "token assign-left variable" > IMAGE< / span > < span class = "token operator" > =< / span > < span class = "token variable" > < span class = "token variable" > $(< / span > < span class = "token function" > cat< / span > $FILE < span class = "token operator" > |< / span > < span class = "token function" > grep< / span > < span class = "token parameter variable" > -E< / span > < span class = "token string" > "name:.+jpg|png$"< / span > < span class = "token operator" > |< / span > < span class = "token function" > cut< / span > < span class = "token parameter variable" > -d< / span > < span class = "token string" > ' '< / span > < span class = "token parameter variable" > -f< / span > < span class = "token number" > 6< / span > -< span class = "token variable" > )< / span > < / span >
< span class = "token assign-left variable" > YEAR< / span > < span class = "token operator" > =< / span > < span class = "token variable" > < span class = "token variable" > $(< / span > < span class = "token builtin class-name" > echo< / span > $FILE < span class = "token operator" > |< / span > < span class = "token function" > cut< / span > < span class = "token parameter variable" > -d< / span > < span class = "token string" > '/'< / span > < span class = "token parameter variable" > -f< / span > < span class = "token number" > 2< / span > -< span class = "token variable" > )< / span > < / span >
< span class = "token comment" > # replace in-place in file< / span >
< span class = "token function" > sed< / span > < span class = "token string" > "s,< span class = "token variable" > $IMAGE< / span > ,< span class = "token variable" > $YEAR< / span > /& ,"< / span > < span class = "token parameter variable" > -i< / span > < span class = "token variable" > $FILE< / span >
< span class = "token comment" > # path creation< / span >
< span class = "token assign-left variable" > IMG_DIR< / span > < span class = "token operator" > =< / span > assets/img
< span class = "token assign-left variable" > NEW_IMAGE< / span > < span class = "token operator" > =< / span > < span class = "token variable" > $IMG_DIR< / span > /< span class = "token variable" > $YEAR< / span > /< span class = "token variable" > $IMAGE< / span >
< span class = "token assign-left variable" > IMAGE< / span > < span class = "token operator" > =< / span > < span class = "token variable" > $IMG_DIR< / span > /< span class = "token variable" > $IMAGE< / span >
< span class = "token comment" > # create dir for year if it doesn't exist< / span >
< span class = "token keyword" > if< / span > < span class = "token punctuation" > [< / span > < span class = "token operator" > !< / span > < span class = "token parameter variable" > -d< / span > < span class = "token variable" > $IMG_DIR< / span > /< span class = "token variable" > $YEAR< / span > < span class = "token punctuation" > ]< / span >
< span class = "token keyword" > then< / span >
< span class = "token function" > mkdir< / span > < span class = "token variable" > $IMG_DIR< / span > /< span class = "token variable" > $YEAR< / span >
< span class = "token keyword" > fi< / span >
< span class = "token comment" > # move image< / span >
< span class = "token function" > mv< / span > < span class = "token variable" > $IMAGE< / span > < span class = "token variable" > $NEW_IMAGE< / span >
< span class = "token keyword" > done< / span > < / code > < / pre >
< p > questions? errors? < a href = "/contact" > ping me!< / a > < / p >
< / article >
< nav aria-label = "pagination" >
< ol class = "pagination post-pagination" >
< li class = "older" >
< a href = "/petrichor-handspun/" >
< i class = "fa-solid fa-hand-point-left" aria-hidden = "true" > < / i > petrichor handspun
< / a >
< / li >
< li class = "newer" >
< a href = "/orion-handspun/" >
orion handspun < i class = "fa-solid fa-hand-point-right" aria-hidden = "true" > < / i >
< / a >
< / li >
< / ol >
< / nav >
< hr >
< section class = "related-posts" >
< h2 data-ha-exclude = "" id = "related-posts" > related posts< / h2 >
< ol id = "postlist" >
< li class = "post" >
2026-06-30 23:29:05 +00:00
< a class = "postlink" href = "/accessible-image-modals/" >
< h2 data-ha-exclude = "" id = "accessible-image-modals" > accessible image modals < / h2 >
2026-05-15 09:20:43 -07:00
< ul class = "postlist-tags" >
< li > software< / li >
< / ul >
2026-06-30 23:29:05 +00:00
< img src = "/img/snacking-seagull.jpg" alt = "Image unrelated to post. A seagull floating in the water with a starfish hanging out of eir mouth." loading = "lazy" decoding = "async" width = "1000" height = "666" >
2026-05-15 09:20:43 -07:00
< / a >
< / li >
< li class = "post" >
2026-06-30 23:29:05 +00:00
< a class = "postlink" href = "/core-css/" >
< h2 data-ha-exclude = "" id = "core-css" > core CSS < / h2 >
2026-05-15 09:20:43 -07:00
< ul class = "postlist-tags" >
< li > software< / li >
< / ul >
2026-06-30 23:29:05 +00:00
< img src = "/img/core-css.png" alt = "A website mockup in dark mode with light pink accents. Everything is filler text such as the title 'This is my cool website', headers reading things like 'This is an h1', nav items 'Nav item 1', etc. There is a paragraph block - the opening of the Bee Movie - and part of an image visible - a screencap of the music video for Never Gonna Give You Up by Rick Astley." loading = "lazy" decoding = "async" width = "1000" height = "447" >
2026-05-15 09:20:43 -07:00
< / a >
< / li >
< li class = "post" >
2026-06-30 23:29:05 +00:00
< a class = "postlink" href = "/designing-a-bag/" >
< h2 data-ha-exclude = "" id = "designing-a-bag" > designing a bag < / h2 >
2026-05-15 09:20:43 -07:00
< ul class = "postlist-tags" >
2026-06-30 23:29:05 +00:00
< li > leather< / li >
2026-05-15 09:20:43 -07:00
< li > software< / li >
< / ul >
2026-06-30 23:29:05 +00:00
< img src = "/img/shoelace-bag.jpg" alt = "a 3-image collage showcasing a leather crossbody bag. the leather body is brown and fairly simple. up the narrow sides, rope is laced through grommets in a style resembling a shoe lacing. the rope forms the handle and loops seamlessly through the other side of the bag, joining in one point in a figure-8 follow-through knot. At the bottom corners, there are small diagonal lines of stitching to give the bag a small lip around the base and ensure small objects don't slide out." loading = "lazy" decoding = "async" width = "1000" height = "1777" >
2026-05-15 09:20:43 -07:00
< / a >
< / li >
< / ol >
< / section >
< / heading-anchors >
< / main >
< footer >
< ul >
< li >
< a href = "/colophon/" >
colophon
< / a >
< / li >
< li >
< a href = "/" title = "go home" aria-label = "go home | hello hello from Lee Cattarin, copyright 2022-2026" >
hello hello from Lee Cattarin, © 2022-2026< / a >
< / li >
< li >
< a href = "https://heckin.technology/inherentlee/leecat.art" title = "source code" aria-label = "source code" target = "_blank" rel = "external" >
src
< / a >
< / li >
< / ul >
< / footer >
2026-06-30 23:29:05 +00:00
<!-- This page `/moving - images/` was built on 2026 - 06 - 30T23:28:51.288Z -->
2026-05-15 09:20:43 -07:00
< / body >
< / html >