Three sites, one directory each
flux.vision, tobias.raayoni-last.com and randogoth.com. They were one site until October 2026, serving four audiences from a single dated list, which is the only format that can hold unrelated things at once. flux.vision is recovered from the Wayback Machine, where it had sat since being deleted in 2021: 90 articles, closed and maintained rather than updated. tobias.raayoni-last.com is the professional record, 97 articles back to 1999. randogoth.com is the technical work, 41 articles across six areas, served as an ANSI bulletin board. History starts here deliberately. The development history, the 266-row inventory that records why each article went where it did, the Wayback pipelines, the pruned articles, the source archives and the full-size images are kept beside the repo rather than in it, so a clone is the web presence and not the argument that produced it. REFACTOR.md is gitignored for the same reason. See README.md for where that material lives, and keep it: the sort is not reproducible from these three directories alone. Each site documents itself in its own README, including the traps that cost time and look like nothing when you get them wrong.
This commit is contained in:
commit
8505b7e9e0
1003 changed files with 13289 additions and 0 deletions
338
tobias.raayoni-last.com/raayoni/static/css/style.css
Normal file
338
tobias.raayoni-last.com/raayoni/static/css/style.css
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
/* raayoni — tobias.raayoni-last.com
|
||||
*
|
||||
* Index after dominofactory.net: a Muuri grid of greyscale-at-rest tiles.
|
||||
* Entry after fabriziopoltronieri.com: fixed parallax hero, narrow centred
|
||||
* column, and a hierarchy that switches typeface by purpose — sans small-caps
|
||||
* for titles, mono for metadata, serif for prose.
|
||||
*
|
||||
* Both references hard-code hex and are dark-only. Everything here goes through
|
||||
* custom properties so the light/dark switch is not a retrofit.
|
||||
*/
|
||||
|
||||
/* ── Lato, self-hosted ───────────────────────────────────────────────────
|
||||
* SIL Open Font License, so redistribution is fine. Self-hosted rather than
|
||||
* loaded from Google's CDN: no third-party request on a site aimed partly at
|
||||
* German and EU cultural institutions, and no external dependency in a static
|
||||
* build. latin-ext is needed for the client names — Kežmarok, szerk, Schmück.
|
||||
*
|
||||
* Lato ships 400, 700 and 900 only. There is no 800 or 600, so the weights
|
||||
* below are snapped to what actually exists rather than letting the browser
|
||||
* synthesise.
|
||||
*/
|
||||
@font-face { font-family: Lato; font-style: normal; font-weight: 400; font-display: swap;
|
||||
src: url(../fonts/lato-400-normal-latin.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
|
||||
@font-face { font-family: Lato; font-style: normal; font-weight: 400; font-display: swap;
|
||||
src: url(../fonts/lato-400-normal-latin-ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; }
|
||||
@font-face { font-family: Lato; font-style: italic; font-weight: 400; font-display: swap;
|
||||
src: url(../fonts/lato-400-italic-latin.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
|
||||
@font-face { font-family: Lato; font-style: italic; font-weight: 400; font-display: swap;
|
||||
src: url(../fonts/lato-400-italic-latin-ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; }
|
||||
@font-face { font-family: Lato; font-style: normal; font-weight: 700; font-display: swap;
|
||||
src: url(../fonts/lato-700-normal-latin.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
|
||||
@font-face { font-family: Lato; font-style: normal; font-weight: 700; font-display: swap;
|
||||
src: url(../fonts/lato-700-normal-latin-ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; }
|
||||
@font-face { font-family: Lato; font-style: normal; font-weight: 900; font-display: swap;
|
||||
src: url(../fonts/lato-900-normal-latin.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
|
||||
@font-face { font-family: Lato; font-style: normal; font-weight: 900; font-display: swap;
|
||||
src: url(../fonts/lato-900-normal-latin-ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; }
|
||||
|
||||
/* ── palette ─────────────────────────────────────────────────────────────
|
||||
* Contrast note: the entry reference sets metadata at #444 and subheads at
|
||||
* #555 on a #111 ground — about 1.9:1 and 2.6:1, which fails WCAG AA badly.
|
||||
* The recessive hierarchy is kept, but the quiet tones are lifted to pass.
|
||||
*/
|
||||
:root {
|
||||
--font-sans: Lato, "Helvetica Neue", Arial, system-ui, sans-serif;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
--font-serif: "Source Serif 4", Charter, Georgia, "Times New Roman", serif;
|
||||
|
||||
--measure: 60%; /* entry column width, per the reference */
|
||||
--accent: #ffa07a; /* lightsalmon, from the reference */
|
||||
}
|
||||
|
||||
:root, [data-theme="dark"] {
|
||||
--ground: #111;
|
||||
--ground-alt: #191919;
|
||||
--title: #aaa;
|
||||
--body: #bbb;
|
||||
--sub: #777; /* lifted from #555 */
|
||||
--meta: #8d8d8d; /* lifted from #444 */
|
||||
--rule: #2a2a2a;
|
||||
--tile: #1a1a1a;
|
||||
--panel-lift: rgba(0,0,0,.6);
|
||||
--thumb-rest: grayscale(100%) contrast(1.05);
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--ground: #f7f6f4;
|
||||
--ground-alt: #efeeea;
|
||||
--title: #2b2b2b;
|
||||
--body: #343434;
|
||||
--sub: #6a6a6a;
|
||||
--meta: #5f5f5f;
|
||||
--rule: #dcdad4;
|
||||
--tile: #eceae5;
|
||||
--panel-lift: rgba(0,0,0,.22);
|
||||
/* grayscale alone reads washed out on a pale ground; a little contrast and
|
||||
a touch of darkening keeps the tile deliberate rather than faded */
|
||||
--thumb-rest: grayscale(100%) contrast(1.12) brightness(.94);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root:not([data-theme]) {
|
||||
--ground: #f7f6f4; --ground-alt: #efeeea; --title: #2b2b2b; --body: #343434;
|
||||
--sub: #6a6a6a; --meta: #5f5f5f; --rule: #dcdad4; --tile: #eceae5;
|
||||
--panel-lift: rgba(0,0,0,.22);
|
||||
--thumb-rest: grayscale(100%) contrast(1.12) brightness(.94);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── base ─────────────────────────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
html, body { margin: 0; min-height: 100%; }
|
||||
|
||||
body {
|
||||
background: var(--ground);
|
||||
color: var(--body);
|
||||
font-family: var(--font-serif);
|
||||
font-size: 1.3em;
|
||||
line-height: 1.6;
|
||||
letter-spacing: .0611111rem;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
transition: background-color .35s ease, color .35s ease;
|
||||
}
|
||||
|
||||
a { color: var(--body); text-decoration: none; border-bottom: 1px solid var(--rule); }
|
||||
a:hover { color: var(--accent); border-bottom-color: var(--accent); }
|
||||
|
||||
h1, h2, h3 { font-family: var(--font-sans); font-weight: 900; text-align: left; margin: 0; }
|
||||
/* Lato has no true small caps, so this is browser-synthesised. At weight 900
|
||||
and display size it holds; for real control swap to:
|
||||
text-transform: uppercase; font-size: 3em; letter-spacing: .02em; */
|
||||
h1 { font-size: 4em; color: var(--title); font-variant-caps: all-small-caps;
|
||||
padding: 3em 0 0; line-height: 1; }
|
||||
h1.tight { padding-top: .35em; }
|
||||
h2 { font-size: 2.2em; color: var(--title); }
|
||||
h3 { font-size: 1.8em; color: var(--sub); padding: 0 15vw 2vh 0; }
|
||||
h4 { font-family: var(--font-mono); font-weight: 400; font-size: 1.2em; color: var(--meta); margin: 0; }
|
||||
|
||||
p { margin: 0; padding: 0 0 3vh; }
|
||||
|
||||
/* ── chrome ───────────────────────────────────────────────────────────── */
|
||||
.bar {
|
||||
position: sticky; top: 0; z-index: 20;
|
||||
display: flex; gap: 1.2rem; align-items: baseline; flex-wrap: wrap;
|
||||
padding: .9rem 5vw; background: color-mix(in srgb, var(--ground) 88%, transparent);
|
||||
backdrop-filter: blur(8px); border-bottom: 1px solid var(--rule);
|
||||
font-family: var(--font-mono); font-size: .72rem; letter-spacing: .04em;
|
||||
}
|
||||
.bar a, .bar button { border: 0; background: none; color: var(--meta); cursor: pointer;
|
||||
font: inherit; padding: 0; text-transform: lowercase; }
|
||||
.bar a:hover, .bar button:hover, .bar .on { color: var(--accent); }
|
||||
.bar .me { color: var(--title); font-weight: 700; margin-right: auto; text-transform: none; }
|
||||
.bar .sep { color: var(--rule); }
|
||||
|
||||
/* ── index grid ───────────────────────────────────────────────────────── */
|
||||
.controls {
|
||||
display: flex; gap: .55rem 1rem; flex-wrap: wrap; align-items: center;
|
||||
padding: 2.2rem 5vw .4rem; font-family: var(--font-mono);
|
||||
font-size: .72rem; letter-spacing: .04em;
|
||||
}
|
||||
.controls .label { color: var(--sub); margin-right: .2rem; }
|
||||
.controls button { background: none; border: 1px solid var(--rule); border-radius: 2px;
|
||||
color: var(--meta); cursor: pointer; font: inherit; padding: .22rem .55rem; }
|
||||
.controls button:hover { color: var(--accent); border-color: var(--accent); }
|
||||
.controls button[aria-pressed="true"] { color: var(--ground); background: var(--title); border-color: var(--title); }
|
||||
.controls .count { color: var(--sub); margin-left: auto; }
|
||||
|
||||
.grid { position: relative; padding: 1rem 5vw 8vh; }
|
||||
.item { display: block; position: absolute; width: 25%; padding: .55rem; z-index: 1; }
|
||||
.item.muuri-item-hidden { z-index: 0; }
|
||||
.item-content { position: relative; }
|
||||
@media (max-width: 1500px) { .item { width: 33.333%; } }
|
||||
@media (max-width: 1000px) { .item { width: 50%; } }
|
||||
@media (max-width: 620px) { .item { width: 100%; } }
|
||||
|
||||
.hoverthumb { position: relative; overflow: hidden; background: var(--tile);
|
||||
aspect-ratio: 4 / 3; border: 1px solid var(--rule); }
|
||||
.hoverthumb img { position: absolute; inset: 0; width: 100%; height: 100%;
|
||||
object-fit: cover; display: block; transition: transform 1.2s ease; }
|
||||
|
||||
/* Hovering a tile dims its siblings rather than colourising the target. Same
|
||||
* job — one thing legible at a time across images of uneven quality — without
|
||||
* needing a greyscale duplicate of every thumbnail. */
|
||||
.grid:hover .item:not(.head) .item-content { opacity: .34; filter: saturate(.55); }
|
||||
.grid .item:not(.head) .item-content:hover { opacity: 1; filter: none; }
|
||||
.item-content { transition: opacity .32s ease, filter .32s ease; }
|
||||
.item-content:hover .hoverthumb img { transform: scale(1.03); }
|
||||
@media (hover: none) { .grid:hover .item .item-content { opacity: 1; filter: none; } }
|
||||
|
||||
/* No surviving image: the tile becomes typographic rather than broken.
|
||||
~35 entries are in this position — see ../../IMAGES.md */
|
||||
.hoverthumb.empty { display: grid; place-items: center; aspect-ratio: 4 / 3; }
|
||||
.hoverthumb.empty span { font-family: var(--font-mono); font-size: .68rem;
|
||||
color: var(--sub); letter-spacing: .12em; text-transform: uppercase; }
|
||||
|
||||
.item .content { padding: .7rem 0 0; }
|
||||
.item .title { font-family: var(--font-sans); font-weight: 700; font-size: 1.05rem;
|
||||
line-height: 1.12; color: var(--title); margin-bottom: .3rem; }
|
||||
.item .year { font-family: var(--font-mono); font-size: .68rem; color: var(--sub); }
|
||||
.item .description { font-size: .82rem; line-height: 1.35; color: var(--body);
|
||||
margin: .3rem 0 .4rem; }
|
||||
.item .tags { font-family: var(--font-mono); font-size: .66rem; color: var(--meta); }
|
||||
.item .tags span { margin-right: .45rem; }
|
||||
.item a.cover { position: absolute; inset: 0; border: 0; }
|
||||
|
||||
/* ── entry ────────────────────────────────────────────────────────────── */
|
||||
/* The hero stops short of the fold and the prose panel rides up over it, so
|
||||
* the page reads as having something below rather than ending at the image.
|
||||
* A full-height hero gave no affordance to scroll at all.
|
||||
*/
|
||||
.hero { height: 82vh; background-attachment: fixed; background-position: 50%;
|
||||
background-repeat: no-repeat; background-size: cover; position: relative; }
|
||||
@media (max-width: 600px) { .hero { background-attachment: scroll; height: 56vh; } }
|
||||
|
||||
.pages { width: 100vw; display: flex; background: var(--ground);
|
||||
position: relative; z-index: 2; }
|
||||
/* The lift only applies when an image hero actually precedes the panel. Putting
|
||||
it on .pages unconditionally pulled the title under the sticky bar on
|
||||
timeline, disciplines and tags, which have no hero at all. */
|
||||
.hero + .pages { margin-top: -5.5rem;
|
||||
box-shadow: 0 -26px 46px -14px var(--panel-lift); }
|
||||
@media (max-width: 600px) { .hero + .pages { margin-top: -3.5rem; } }
|
||||
|
||||
/* Listing pages start at the top of the document, so they need clearance from
|
||||
the sticky bar in their own right. */
|
||||
.pages > .frame > h1.tight:first-child { padding-top: 2.6rem; }
|
||||
.hero + .pages > .frame > h1.tight:first-child,
|
||||
.markplate + h1.tight { padding-top: .35em; }
|
||||
|
||||
/* a quiet cue on the hero itself */
|
||||
.scrollcue { position: absolute; left: 50%; bottom: 6.6rem; transform: translateX(-50%);
|
||||
z-index: 3; font-family: var(--font-mono); font-size: .62rem; letter-spacing: .22em;
|
||||
text-transform: uppercase; color: var(--title); opacity: .75;
|
||||
text-shadow: 0 1px 10px rgba(0,0,0,.65); pointer-events: none;
|
||||
animation: cue 2.6s ease-in-out infinite; }
|
||||
@keyframes cue { 0%,100% { transform: translate(-50%,0); } 50% { transform: translate(-50%,6px); } }
|
||||
.frame { width: var(--measure); display: block; margin: auto; }
|
||||
@media (max-width: 600px) { .frame { width: 100%; } }
|
||||
|
||||
.text { padding: 5vh 0 20vh; }
|
||||
@media (max-width: 600px) {
|
||||
h1 { font-size: 2.5em; padding: 15vw 5vw 0; }
|
||||
h2 { font-size: 1.8em; padding-left: 5vw; }
|
||||
h3 { font-size: 1.4em; padding: 2vh 5vw 0; }
|
||||
h4 { padding: 2vw 5vw 0; }
|
||||
p { font-size: 1em; padding: 2vh 5vw; }
|
||||
.text { padding: 5vh 0; }
|
||||
}
|
||||
|
||||
/* the credit line — For / HiredBy / With / PhotosBy, set in mono */
|
||||
.credits { font-family: var(--font-mono); font-size: .78rem; line-height: 1.75;
|
||||
color: var(--meta); padding: 0 0 3vh; }
|
||||
.credits .rel { color: var(--sub); }
|
||||
.credits a { border-bottom-color: var(--rule); }
|
||||
@media (max-width: 600px) { .credits { padding: 0 5vw 3vh; } }
|
||||
|
||||
.entry-meta { font-family: var(--font-mono); font-size: .72rem; color: var(--sub);
|
||||
letter-spacing: .06em; padding: .4rem 0 2vh; }
|
||||
.entry-meta a { border: 0; color: var(--meta); }
|
||||
.entry-meta a:hover { color: var(--accent); }
|
||||
@media (max-width: 600px) { .entry-meta { padding-left: 5vw; } }
|
||||
|
||||
.body img { max-width: 100%; height: auto; display: block; margin: 0 0 3vh; }
|
||||
.body iframe, .body embed { max-width: 100%; border: 0; margin: 0 0 3vh; }
|
||||
.body h2 { font-size: 1.5em; padding: 2vh 0 1vh; }
|
||||
|
||||
/* ── listings ─────────────────────────────────────────────────────────── */
|
||||
.rows { list-style: none; margin: 0; padding: 0 0 8vh; }
|
||||
.rows li { display: flex; gap: 1.2rem; align-items: baseline; padding: .55rem 0;
|
||||
border-bottom: 1px solid var(--rule); }
|
||||
.rows .yr { font-family: var(--font-mono); font-size: .72rem; color: var(--sub);
|
||||
min-width: 3.4rem; }
|
||||
.rows .ti { font-family: var(--font-sans); font-weight: 700; font-size: 1rem;
|
||||
color: var(--title); }
|
||||
.rows .ct { font-family: var(--font-mono); font-size: .66rem; color: var(--meta);
|
||||
margin-left: auto; }
|
||||
.rows a { border: 0; }
|
||||
.rows a:hover .ti { color: var(--accent); }
|
||||
|
||||
.lede { padding: 1vh 0 4vh; color: var(--sub); font-size: 1.05em; }
|
||||
.cloud { padding: 0 0 8vh; font-family: var(--font-mono); font-size: .8rem; line-height: 2.2; }
|
||||
.cloud a { margin-right: 1rem; }
|
||||
|
||||
footer { border-top: 1px solid var(--rule); padding: 3vh 5vw 6vh;
|
||||
font-family: var(--font-mono); font-size: .7rem; color: var(--sub); }
|
||||
footer a { border: 0; color: var(--meta); }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { transition: none !important; animation: none !important; }
|
||||
.hero { background-attachment: scroll; }
|
||||
}
|
||||
|
||||
/* ── the mark ─────────────────────────────────────────────────────────────
|
||||
* The swift, as used on invoices. Inlined so fill="currentColor" resolves,
|
||||
* which means it inverts with the theme from one asset: white on the dark
|
||||
* ground, black on the light one.
|
||||
*/
|
||||
.bar .me { display: inline-flex; align-items: center; gap: .55rem; }
|
||||
.mark { height: 1.45rem; width: auto; display: block; color: var(--title); }
|
||||
.bar .me:hover .mark { color: var(--accent); }
|
||||
|
||||
/* Larger on the about page, where it is the identity rather than navigation. */
|
||||
.markplate { display: block; margin: 0 0 2vh; }
|
||||
.markplate .mark { height: 5.5rem; }
|
||||
@media (max-width: 600px) { .markplate { padding-left: 5vw; } }
|
||||
|
||||
/* ── engagement grouping ──────────────────────────────────────────────────
|
||||
* Entries belonging to a long relationship sit together under a full-width
|
||||
* header. ClipAward is nine entries over four years spanning design,
|
||||
* projection and animation — a flat grid cannot say that.
|
||||
*/
|
||||
.item.head { width: 100%; padding: 2.4rem .55rem .5rem; }
|
||||
.item.head:first-child { padding-top: .6rem; }
|
||||
.grouphead { border-bottom: 1px solid var(--rule); padding-bottom: .5rem;
|
||||
display: flex; gap: 1rem; align-items: baseline; flex-wrap: wrap; }
|
||||
.gname { font-family: var(--font-sans); font-weight: 900; font-size: 1.45rem;
|
||||
color: var(--title); font-variant-caps: all-small-caps; letter-spacing: .01em; }
|
||||
.gmeta { font-family: var(--font-mono); font-size: .68rem; color: var(--sub);
|
||||
letter-spacing: .04em; }
|
||||
/* Flat arrangements hide the headers; so does any active filter. */
|
||||
.grid:not([data-grouped]) .item.head { display: none; }
|
||||
|
||||
/* ── contact ──────────────────────────────────────────────────────────────
|
||||
* The address is the point of this page, so it is set at display size rather
|
||||
* than buried in a list. The old page hid it behind a document.write()
|
||||
* obfuscation that Markdown rendered as visible source, so it was neither
|
||||
* clickable nor hidden.
|
||||
*/
|
||||
.bigmail { font-family: var(--font-sans); font-weight: 700; font-size: 1.9em;
|
||||
line-height: 1.2; padding: 1vh 0 3vh; word-break: break-word; }
|
||||
.bigmail a { border-bottom-width: 2px; }
|
||||
|
||||
.profiles { list-style: none; margin: 0; padding: 2vh 0 3vh;
|
||||
font-family: var(--font-mono); font-size: .82rem; }
|
||||
.profiles li { display: flex; gap: .9rem; align-items: baseline; padding: .3rem 0; }
|
||||
.profiles .rel { color: var(--sub); min-width: 6.2rem; }
|
||||
.profiles a { border-bottom-color: var(--rule); }
|
||||
@media (max-width: 600px) {
|
||||
.bigmail, .profiles { padding-left: 5vw; padding-right: 5vw; }
|
||||
.bigmail { font-size: 1.4em; }
|
||||
}
|
||||
/* no-JS fallback wording, kept quiet so it does not read as emphasis */
|
||||
.bigmail i { font-style: normal; color: var(--sub); font-size: .62em;
|
||||
font-family: var(--font-mono); padding: 0 .15em; }
|
||||
|
||||
/* Static pages start directly under the bar: no hero, so they need their own
|
||||
top clearance rather than inheriting an empty 30vh placeholder. */
|
||||
.pages > .frame > .markplate:first-child { padding-top: 3.2rem; }
|
||||
.pages > .frame > .markplate:first-child + h1.tight { padding-top: .1em; }
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
tobias.raayoni-last.com/raayoni/static/img/mark.svg
Normal file
1
tobias.raayoni-last.com/raayoni/static/img/mark.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 161.36533 268.29031" role="img" aria-label="Tobias Raayoni Last"><g transform="translate(-150.26933,-390.24689)"><path fill="currentColor" transform="matrix(0.13333333,0,0,-0.13333333,0,816)" d="m 1127.08,2003.92 -0.06,-0.3 c 10.57,-3.95 107.03,-39.95 161.26,-55.57 53.65,-15.46 85.22,-23.27 89.12,-24.24 l 0.01,-0.25 c -7.26,-2.64 -111.38,-40.61 -161.15,-64.09 l -1.53,-0.72 c -12.12,-5.72 -6.89,-23.99 6.42,-22.46 75.33,8.66 191.92,21.61 236.48,24.16 67.75,3.87 248.11,-12.47 250.1,-12.65 l 0.03,-0.02 c 0.27,-0.96 11.45,-41.19 -6.82,-68.11 -18.49,-27.25 -180.05,-218.01 -216.07,-265.71 -36,-47.68 -71.04,-112.89 -98.29,-146.96 -27.25,-34.06 -95.38,-115.81 -124.58,-138.2 -29.2,-22.38 -60.34,-41.85 -53.53,-46.71 6.81,-4.87 76.89,6.81 131.39,22.38 54.51,15.58 241.37,109.98 327.99,175.19 86.62,65.2 189.79,154.09 247.21,212.81 57.42,58.73 120.68,135.62 156.7,184.28 35.97,48.62 38.92,62.26 39.89,91.41 l 0.02,0.08 c 1.65,0.34 126.77,26.55 165.44,69.09 l 37.44,41.2 c 0.99,1.08 2.16,1.97 3.47,2.62 l 19.22,9.6 0.02,0.27 -30.93,23.2 -0.02,0.01 c -0.19,0.22 -29.36,36.08 -78.84,59.36 -46.86,22.06 -62.49,25.89 -64.09,26.25 l -0.12,0.1 c 1.18,3.26 42.8,118.27 42.8,158.58 0,40.87 -44.77,157.66 -81.75,229.68 -36.99,72.02 -114.85,222.88 -140.15,264.73 -25.3,41.85 -115.82,175.19 -185.89,241.37 -70.08,66.18 -199.52,190.76 -238.46,198.54 -38.92,7.79 70.08,-134.3 70.08,-162.53 0,-28.23 35.04,-79.81 36.99,-109 1.95,-29.2 26.27,-90.52 30.17,-114.85 3.89,-24.33 41.48,-131.74 41.48,-131.74 0,0 7.18,-40.53 35.41,-127.15 28.09,-86.21 81.25,-292.94 81.74,-294.88 l -0.03,-0.07 -119.93,-206.33 -0.06,-0.07 c -1.77,-0.23 -111.23,-14.59 -177.75,-32.72 -66.63,-18.18 -97.16,-29.58 -97.56,-29.73 h -0.04 l -333.23,20.15"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
21
tobias.raayoni-last.com/raayoni/static/js/muuri.min.js
vendored
Normal file
21
tobias.raayoni-last.com/raayoni/static/js/muuri.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
169
tobias.raayoni-last.com/raayoni/static/js/site.js
Normal file
169
tobias.raayoni-last.com/raayoni/static/js/site.js
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
/* raayoni — grid, filtering, sorting, light/dark.
|
||||
*
|
||||
* The grid is Muuri (muuri.dev), as on dominofactory.net. Filtering is by
|
||||
* discipline (Category) and by engagement (Project) — two independent axes, so
|
||||
* they compose. Sorting is by year, which spans 2000–2022.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
/* ── light / dark ──────────────────────────────────────────────────────
|
||||
* The initial value is applied by an inline script in <head> so there is no
|
||||
* flash of the wrong ground colour before this file loads. Here we only
|
||||
* handle the toggle and keep the button label honest.
|
||||
*/
|
||||
var root = document.documentElement;
|
||||
|
||||
function currentTheme() {
|
||||
return root.getAttribute("data-theme") ||
|
||||
(window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
|
||||
}
|
||||
|
||||
function paintToggle(btn) {
|
||||
if (!btn) return;
|
||||
var dark = currentTheme() === "dark";
|
||||
btn.textContent = dark ? "light" : "dark";
|
||||
btn.setAttribute("aria-label", "Switch to " + (dark ? "light" : "dark") + " mode");
|
||||
}
|
||||
|
||||
var toggle = document.querySelector("[data-theme-toggle]");
|
||||
paintToggle(toggle);
|
||||
if (toggle) {
|
||||
toggle.addEventListener("click", function () {
|
||||
var next = currentTheme() === "dark" ? "light" : "dark";
|
||||
root.setAttribute("data-theme", next);
|
||||
try { localStorage.setItem("theme", next); } catch (e) { /* private mode */ }
|
||||
paintToggle(toggle);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* ── contact address ──────────────────────────────────────────────────────
|
||||
* The address is stored rot13'd, so the page source contains no string an
|
||||
* email regex will match, and is written out as a real mailto only once the
|
||||
* document is live. Without JS the visible fallback stays human-readable
|
||||
* ("tobias at raayoni-last dot com") rather than silently breaking.
|
||||
*
|
||||
* This stops regex harvesters, not a scraper that executes JS. The previous
|
||||
* version used document.write inside Markdown, which rendered its own source
|
||||
* as visible text and was neither clickable nor hidden.
|
||||
*/
|
||||
Array.prototype.forEach.call(document.querySelectorAll(".mail[data-r]"), function (el) {
|
||||
var addr = el.getAttribute("data-r").replace(/[a-zA-Z]/g, function (c) {
|
||||
return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
|
||||
});
|
||||
var a = document.createElement("a");
|
||||
a.href = "mailto:" + addr;
|
||||
a.textContent = addr;
|
||||
el.replaceWith(a);
|
||||
});
|
||||
|
||||
/* ── the grid ─────────────────────────────────────────────────────────── */
|
||||
var el = document.querySelector(".grid");
|
||||
if (!el || typeof Muuri === "undefined") return;
|
||||
|
||||
var grid = new Muuri(el, {
|
||||
items: ".item",
|
||||
layout: { fillGaps: false, rounding: true },
|
||||
layoutDuration: 280,
|
||||
layoutEasing: "cubic-bezier(.2,.7,.3,1)",
|
||||
sortData: {
|
||||
year: function (item, element) {
|
||||
return parseInt(element.getAttribute("data-year"), 10) || 0;
|
||||
},
|
||||
title: function (item, element) {
|
||||
return (element.getAttribute("data-title") || "").toLowerCase();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* Tiles are images of wildly varying size fetched from an archive, so the
|
||||
first layout is usually wrong until they have decoded. Relayout as they
|
||||
arrive rather than guessing dimensions up front. */
|
||||
Array.prototype.forEach.call(el.querySelectorAll("img"), function (img) {
|
||||
if (img.complete) return;
|
||||
img.addEventListener("load", relayout, { once: true });
|
||||
img.addEventListener("error", function () {
|
||||
img.closest(".hoverthumb").classList.add("empty");
|
||||
relayout();
|
||||
}, { once: true });
|
||||
});
|
||||
var pending = null;
|
||||
function relayout() {
|
||||
clearTimeout(pending);
|
||||
pending = setTimeout(function () { grid.refreshItems().layout(); }, 60);
|
||||
}
|
||||
window.addEventListener("load", relayout);
|
||||
|
||||
/* ── grouping, filtering, sorting ─────────────────────────────────────── */
|
||||
/* Headers are grid items too, so they must be hidden whenever the grid is
|
||||
not in its grouped arrangement — a filtered or re-sorted grid would
|
||||
otherwise strand them above the wrong tiles. */
|
||||
function grouped(on) {
|
||||
if (on) { el.setAttribute("data-grouped", "true"); }
|
||||
else { el.removeAttribute("data-grouped"); }
|
||||
}
|
||||
function isGrouped() { return el.hasAttribute("data-grouped"); }
|
||||
|
||||
var active = { category: "", project: "" };
|
||||
var countEl = document.querySelector("[data-count]");
|
||||
|
||||
function apply() {
|
||||
var filtering = !!(active.category || active.project);
|
||||
if (filtering) grouped(false);
|
||||
grid.filter(function (item) {
|
||||
var e = item.getElement();
|
||||
if (e.classList.contains("head")) return isGrouped() && !filtering;
|
||||
var okCat = !active.category || e.getAttribute("data-category") === active.category;
|
||||
var okPrj = !active.project || e.getAttribute("data-project") === active.project;
|
||||
return okCat && okPrj;
|
||||
});
|
||||
if (countEl) {
|
||||
var n = grid.getItems().filter(function (i) {
|
||||
return i.isVisible() && !i.getElement().classList.contains("head");
|
||||
}).length;
|
||||
countEl.textContent = n + (n === 1 ? " entry" : " entries");
|
||||
}
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(document.querySelectorAll("[data-filter]"), function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var axis = btn.getAttribute("data-axis") || "category";
|
||||
var val = btn.getAttribute("data-filter");
|
||||
active[axis] = active[axis] === val ? "" : val; /* click again to clear */
|
||||
document.querySelectorAll('[data-axis="' + axis + '"]').forEach(function (b) {
|
||||
b.setAttribute("aria-pressed", String(b.getAttribute("data-filter") === active[axis]));
|
||||
});
|
||||
apply();
|
||||
});
|
||||
});
|
||||
|
||||
/* ── sorting ──────────────────────────────────────────────────────────── */
|
||||
Array.prototype.forEach.call(document.querySelectorAll("[data-sort]"), function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var how = btn.getAttribute("data-sort");
|
||||
if (how === "group") {
|
||||
active.category = active.project = "";
|
||||
document.querySelectorAll("[data-filter]").forEach(function (b) {
|
||||
b.setAttribute("aria-pressed", "false");
|
||||
});
|
||||
grouped(true);
|
||||
grid.sort(function (a, b) { return a._dom - b._dom; }); /* DOM order */
|
||||
} else {
|
||||
grouped(false);
|
||||
grid.sort(how === "year-asc" ? "year:asc" : how === "title" ? "title:asc" : "year:desc");
|
||||
}
|
||||
document.querySelectorAll("[data-sort]").forEach(function (b) {
|
||||
b.setAttribute("aria-pressed", String(b === btn));
|
||||
});
|
||||
apply();
|
||||
});
|
||||
});
|
||||
|
||||
/* The grid is rendered grouped by engagement so that arrangement can be
|
||||
restored from DOM order; newest is the default view, so sort on init. */
|
||||
grid.getItems().forEach(function (item, i) { item._dom = i; });
|
||||
grid.sort("year:desc");
|
||||
|
||||
apply();
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue