Building a Rule-Based Generative Design System in WordPress

Abstract

This article examines how redesigning my personal WordPress website developed into a generative visual identity. Its central question is how typography, colour and composition can vary while recognition, readability and maintainability remain dependable. Drawing on the conversation, screenshots, reversals, original source and recorded checks, I reconstruct how that question emerged. Missing tags first required distinguishing content from display logic; oversized photographs challenged my association between importance and scale; and excessive colour variation led me to propose coherent palettes for each page. Separating typefaces from treatments then made a generator thinkable, while four-character Chinese subsets made a broader catalogue practical. Japanese font comparisons distinguished additional names from additional visible forms. The miniature footer raised a further question about whether shared appearance meant shared implementation. Code inspection supports a qualified answer: the footer derives an edition from the header, whereas palette definitions remain duplicated. AI contributed proposals, code and checks; my objections repeatedly changed their objective. The contribution is a documented account of judgments becoming executable constraints, including the later revisions made possible by examining that code. Motion control, small-scale legibility, visitor response and confirmation of the latest live deployment remain unresolved. Equations make selection and timing assumptions explicit.

Keywords: generative design; visual identity; human–AI collaboration; typography; font subsetting; iterative design; software maintainability.

Before installing a miniature version of my moving logo in the footer of my website, I asked a practical question: if I later changed the main logo, would I have to make the same change twice? I wanted the footer to draw from the main design automatically. It should occupy the small space already used by my name, move more quietly, and remain recognisably part of the same system.

That question brought together a much longer sequence of experiments. I had been redesigning article listings, photographs, navigation, the welcome text, the outer frame and the footer. Along the way, I repeatedly asked for more alternatives, stronger typography, different shadows and more varied colours. I also rejected several results for being excessive. By the time I reached the footer logo, the problem had become quite precise: how could the website keep producing something unfamiliar while retaining a visual identity I recognised as mine?

I did not begin with a theory of generative design. I began with things that bothered me on the screen. A button moved when the neighbouring text became longer. Photographs were too small, then much too large. Article introductions occupied space that I wanted to give to titles. Colours became more varied than I wanted to see together. The eventual system emerged through those objections, with AI helping to propose alternatives and translate decisions into code. Its most interesting feature is therefore not the number of possible combinations. It is the gradually discovered set of limits within which those combinations can make sense.

I can describe those changes as a developing system now. During the work, I was usually attending to the next thing that looked wrong, or asking whether an appealing result could be pushed further. I often accepted the general direction while rejecting a particular position, scale or effect. Those partial agreements mattered: they preserved something worth developing without making acceptance of a proposal an all-or-nothing decision. The account that follows begins with the later maintenance question, but the earlier corrections explain how I became able to ask it. The earlier title’s suggestion that the website “learned” refers to this accumulation of revisions. The runtime does not learn from visitors or update its own design criteria. What changed was the understanding brought to it, and the rules through which that understanding could recur on another page load.

Reconstructing the work became another part of it. An earlier account explained the design convincingly but left too much of the implementation outside the article. I asked for the original code and the architecture because claims about reuse, coverage and constraints should be available for inspection. That request exposed qualifications the visual narrative had compressed. In particular, coordinated colours do not yet come from one central palette definition. This was a discovery made while documenting the work, not an architectural objection I had already formulated when choosing the colours (Yin with ChatGPT, 2026).

Learning what I meant by a stronger design

The inquiry began before the Subject Atlas, with a question about whether Penscratch could already show article tags. I had supplied the article-header markup, which did not show them. The assistant found that tags were present farther down the article, then added them near the title at my request. Categories followed. The first result inherited excessive margins from the existing metadata styling. Something technically present could still be hard to find, and something newly visible could still be poorly arranged. That progression took the conversation from content organisation to a more deliberate editorial treatment.

I liked three proposed article headers—Editorial, Research Index and Poster—and asked whether a visit could choose among them automatically. The assistant proposed keeping the reading surface steady and offering a discreet layout switch. I accepted the automatic variation and declined the switch. Later I reported that the new headers lacked tags, then realised I had opened a post with no tags assigned and withdrew the complaint. Other summary problems were real: the existing archive condition needed is_tag(), and later is_author(), so those listings would not display complete articles. These were different diagnoses. My mistake mattered because a plausible report of missing output was insufficient reason to change the rendering code.

The first split-journal homepage supplied another concrete correction. Featured images left too much empty space, so I suggested a textual default with a photograph appearing on hover. I chose a floating-card proposal, but the assistant interpreted my request for a stronger brutalist treatment as permission to tilt the image across the title. I rejected the tilt and overlap. Using “Darkness” in the Blade Runner title as a positional reference, I asked for the photograph above that part of the heading, with a clear gap. The proposal’s useful interaction survived; its spatial interpretation changed. The later title wall retained this history of treating images as an optional encounter rather than allowing them to displace the words.

When I asked for an overview of subjects, the assistant first recommended connections between tags based on shared articles. I also wanted every topic visible, including isolated ones, and newly used tags to appear without manually rebuilding the page. The overview of aligned boxes and lines then felt cluttered. A typographic cloud offered a different way to preserve an overall view while revealing relationships through selection. The change also contained a trade-off: the initial index proposal kept names equally readable, whereas the cloud proposal used frequency to vary their size. That made frequency visible; it did not establish that my most frequent subject was intellectually the most important.

The results below the cloud needed to explain their own incompleteness. I asked for five articles initially and five more with each click, with a sufficiently visible count and action so that readers would not mistake the first results for the entire collection. I wanted transitions, then objected when the transition felt like flickering. The revised code kept the text fully opaque, changed the list height and used a two-pixel movement over 200 milliseconds. This was a correction of the kind of movement, not a withdrawal of the request for continuity. It respected reduced-motion preferences and cancelled superseded animations when another selection arrived. The reset control became the next difficulty because a reader also needed a clear way out of a narrow selection.

An early irritation concerned the reset control in the Subject Atlas. Its position followed the length of the selected-topic text. I wanted it in a stable place near the cloud, eventually at the bottom right, and visible when a selection made it useful. Once the position was acceptable, I still objected to the styling. The control needed enough size and colour to be recognisably clickable, without becoming an oversized announcement. Removing the neighbouring “Shared articles” action also helped clarify what this part of the interface was supposed to do.

This was a small problem, but it established a distinction that kept returning. Stable structure and expressive appearance had different jobs. I could enjoy changing colours, heavy outlines and displaced shadows while still expecting a control to stay where I could find it. The later desire for a changing logo did not mean that every part of the interface should behave unpredictably.

The archive initially raised a different question: where was its specific design actually implemented? It was a separately added page for all the articles, and I wanted to locate the relevant source before changing it. Its old introduction and category strip, including repeated category names and labels in several languages, seemed unnecessary. My request was to bring this page into the visual world of the Subject Atlas. Once the archive’s contents became the focus, however, a general stylistic update proved too broad a description of the task. Different kinds of entries needed different treatment.

The archive made another limit visible. Many older entries were photographs without titles or explanatory text. Their small thumbnails gave them little presence, so I asked for larger images, centred presentation for photograph-only entries, support for multiple photographs, and the return of the negative-colour hover effect. I wanted to preserve the divisions by year. At first, I described the desired enlargement very strongly, even suggesting full-width images. The resulting scale was too much. I restored the previous version using the supplied rollback command and then reformulated the request: enlarge the photographs only slightly, and apply that change specifically to entries without titles. Posts with titles were to retain their normal article treatment.

The installer’s report makes the sequence unusually clear. It announced that the archive template check had passed and that year sections, large photographs, responsive galleries and negative hover had been installed. My next response was that the photographs were too large. After I ran the undo command, the terminal reported that the previous archive template had been restored. The successful check had answered whether the prepared template passed its technical gate. It had not answered whether the enlargement was proportionate. The screenshot and my experience of the result supplied evidence of a different kind, and that evidence changed the specification.

These selected lines preserve the installation and the subsequent undo as separate events. Their order matters more than the installer’s confidence:

Archive template check passed.
Installed: year sections, large photos, responsive galleries and negative hover.
Previous archive template restored.

That correction mattered more than a new pixel value. I had initially connected the importance of the photographs with their size. Seeing the result showed me that importance also depended on proportion, grouping and the surrounding space. A series of photographs needed a considered arrangement, not a procession of enormous images. The empty-title distinction was also specific to my publishing history; it was a rule for recognising these entries on my site, rather than a general definition of photographic content. The rollback allowed the failed interpretation to remain useful without forcing me to keep its consequences.

I also clarified what should survive the reversal. The negative hover effect was good; the year divisions should remain. Some untitled photograph posts could contain a little text, and some contained several photographs, so an image alone was not an adequate basis for applying a new layout indiscriminately. The revised request narrowed both the degree of enlargement and the entries to which it applied. I was correcting my own earlier instruction about size as well as the implementation’s scope. The useful outcome was a more discriminating brief. The narrower patch specified a maximum image edge of 144 pixels on the Atlas layout and 112 pixels on phones. Those values recorded the revised candidate; they were not measurements proving that this scale was ideal for every photograph.

The examples I supplied made the multiple-photograph requirement concrete. One entry from 2015 contained nine black-and-white photographs of a desk; another contained five street photographs. Other entries showed pairs or triplets. The absence of a title did not make these entries empty. The photographs, and the relationships between them, were their content. A layout that enlarged one isolated image acceptably could still overwhelm the page when applied to a longer sequence. These examples explain why the request concerned galleries as well as image dimensions, and why preserving the chronological grouping mattered alongside changing the visual treatment.

On the homepage, the imbalance ran in the opposite direction. The opening paragraphs beside the article titles occupied too much space. I began to imagine the titles carrying the layout themselves. When I asked for a left–right arrangement, I had to clarify that I meant two aligned, connected boxes. I did not want entries zigzagging down the page as separate left- and right-aligned objects. That spatial distinction led towards the version we called a Poster Wall: adjacent areas in which the title could behave like a substantial piece of typography.

The actual titles help explain that intuition. “Wright’s Fallingwater, the question of privilege and the conditions of a good life” already offered several lines of substantial text, while its neighbouring introduction began by recalling how long I had remembered the house. Other entries, including “Social Media, Fragmented Attention (and My Distraction), and Thinking With AI”, likewise had titles capable of occupying a significant area. I was questioning the homepage’s allocation of attention, rather than revising the articles themselves. Hiding the excerpts would allow those existing titles to become the main material of the composition; it did not require inventing shorter advertising slogans to replace them.

Here the examples changed what I could specify. “Left–right” had seemed sufficient to express my intention until I encountered a direction I did not want. I then described the relationship more exactly: two boxes, aligned and connected. Once that was established, I could ask a different question—whether to remove the outer side borders, highlight the whole block on hover, or make the composition more artistic. The basic arrangement had to become clear before those variations could be assessed. More alternatives were useful when they revealed an unresolved choice; they were less useful when they repeated a misunderstanding of the arrangement.

The names used for the alternatives—Poster Wall, Bound Wall, Index Wall, Offset Print and Fine Cut—were working descriptions that helped us compare proposals. I do not need to turn them into established design categories. They were useful because they gave me something concrete to accept or refuse. I tried a combination of the Bound Wall and Index Wall approaches, restored the previous homepage after seeing it, and returned to a simpler Bound Wall. I wanted a visible frame and some of the roughness I associated with my existing brutalist style, while keeping the titles readable and the page coherent.

The title treatment followed a similarly selective path. I liked the direction of a cut shadow, but continued asking for another kind of that effect before choosing Fine Cut. Asking for less visual weight did not mean abandoning the character I had requested. Conversely, asking for more brutalist character did not mean approving every additional line or shadow. These revisions made “brutalism” function as a starting preference whose practical meaning still had to be worked out at the scale of each component.

The palette changed what randomness meant

The Poster Wall suggested a further possibility: different backgrounds and corresponding text colours could make each article block feel like a printed sheet. I asked for more random combinations, including changes on refresh. When some colours repeated, I initially wanted the system to become more varied. Then I reconsidered. The greater variety was beginning to produce too much activity within a single page.

The first diagnosis had been repetition: perhaps the set was too small, or the selection insufficiently varied. Increasing variety addressed that concern and exposed another. I still liked the general design, but I no longer wanted all those differences competing at once. My objection was therefore not a rejection of the experiment. It identified the scale at which the experiment should operate. The palette-grouping proposal came from this response to the result, rather than from a complete design plan I had supplied at the beginning.

My next proposal was to organise the colours into sets. Each page load would use one coherent palette, and a later refresh could select another. This moved the variation to a different level. There could still be contrasting blocks, but their colours would belong to the same visual world. The surprise would occur between editions of the page, while the relationships within an edition would remain intelligible.

The twelve-pair experiment had made the excess visible. The eventual implementation offered eight families—Acid Paper, Cobalt Print, Warm Clay, Violet Ink, Forest, Rose, Ochre and Slate—each organised around paper, tint, accent and deep ink. My request established the page-level grouping; the assistant developed the four-tone structure and expanded the families when I asked for more. The original selection code records that division clearly:

const pick = values =>
    values[Math.floor(Math.random() * values.length)];

  // One family for the whole page. Avoid repeating the previous page load.
  const key = 'yin-poster-palette-family-v1';
  let lastFamily = '';

  try {
    lastFamily = sessionStorage.getItem(key) || '';
  } catch (_) {}

  const palette = pick(
    palettes.filter(p => p.name !== lastFamily)
  );

Within a family, eight-card patterns used [3, 2, 2, 1] or [2, 3, 2, 1]: five quieter paper or tinted panels, two accents and one deep-ink panel. A tone was not repeated at a distance of one or two positions, and a partial final group favoured lighter panels. The choices addressed the connected two-column layout rather than maximising the number of possible colour assignments. These precise limits were the assistant’s implementation of the direction we had developed; I had not supplied them as a finished specification.

The arrangement is generated by a small recursive search, rather than by shuffling eight colours and hoping the result respects the brief. The function consumes a remaining-count vector. At each position it rejects exhausted tones and tones used in either of the two previous positions; only complete rows enter the candidate collection:

const patterns = [];

function build(row, remaining) {
  if (row.length === 8) {
    patterns.push(row);
    return;
  }

  for (let tone = 0; tone < 4; tone++) {
    if (!remaining[tone] ||
        row[row.length - 1] === tone ||
        row[row.length - 2] === tone) continue;

    const next = remaining.slice();
    next[tone]--;

    build([...row, tone], next);
  }
}

build([], [3, 2, 2, 1]);
build([], [2, 3, 2, 1]);

For a row \(t_1,\ldots,t_8\), the following notation describes the constraints already implemented. Here \(c_k\) is the required count of tone \(k\), and \(\mathbf{1}\) is an indicator equal to one when its condition holds. These equations are my later explanation of the source, not mathematical notation supplied in the original brief:

\[
\begin{aligned}
t_i &\in \{0,1,2,3\},\\
\sum_{i=1}^{8}\mathbf{1}[t_i=k] &=c_k,
\quad c\in\{(3,2,2,1),(2,3,2,1)\},\\
t_i &\ne t_{i-1}\quad(i\ge2),\\
t_i &\ne t_{i-2}\quad(i\ge3).
\end{aligned}
\]

Valid blocks also have to meet valid neighbours. Otherwise, two internally acceptable eight-card patterns could create a repeated tone where they join. Before choosing the next block, the original code checks that boundary and avoids repeating the previous block when another admissible choice exists:

let choices = patterns.filter(p =>
  p[0] !== layout[end - 1] &&
  p[0] !== layout[end - 2] &&
  p[1] !== layout[end - 1]
);

const varied = choices.filter(p =>
  p.join('') !== previousBlock
);
if (varied.length) choices = varied;

The partial-block rule then seeks at least \(\lceil0.6m\rceil\) quiet panels among the \(m\) positions still needed. If that threshold is unavailable among the boundary-compatible candidates, it chooses those with the largest quiet count. This is a preference with an explicit fallback, whereas the neighbour exclusions are constraints on eligibility. That distinction prevents an aesthetic intention such as “predominantly light” from being reported as an unconditional guarantee.

That was one of the clearest changes in the inquiry. I had been asking for more randomness as though the quantity of variation were itself the objective. What I actually preferred was variation with a shared context. The page could change its character without looking as though each element had made an unrelated decision. This gave the later logo generator a useful precedent: select a coherent set of ingredients, then allow controlled differences within it.

Once the main blocks had this character, other parts of the site began to look disconnected. The navigation needed to participate in the same palette and print-like language. I chose connected tabs with an offset treatment. The outer frame also needed to follow the selected colours, with a much more substantial mount along the top, right and bottom. For the footer, I preferred the Corner Stamp direction, kept my name relatively small, and wanted the background information available as text. These were different components with different purposes, but they needed to feel as though they belonged to the same page. The first outer-mount treatment was still too thin. My request for more substantial top, right and bottom edges kept the existing asymmetry, with a 60-pixel mount and a smaller 14-pixel left offset in the supplied desktop geometry. Strengthening the frame was a decision about its relation to the page, not a general instruction to thicken every line.

The footer request also put a limit on simplification. I specifically wanted the text about the background to remain visible, rather than be hidden behind an “About the background” disclosure. Earlier, I had wanted an unnecessary archive category strip removed and a distracting action taken out of the Subject Atlas. These decisions were compatible because they concerned the usefulness of particular elements. There was no general instruction to remove text wherever possible. In the footer, the information mattered, while the name could remain modest in size.

The viewing context affected these judgments too. When I could only see what appeared to be a responsive menu treatment, I asked for a desktop version before choosing the connected tabs. After accepting their arrangement, I still wanted more of the border and offset character associated with my site. Later, the pagination raised a functional question alongside the visual one: could a click append more article boxes instead of reloading the whole page? I chose the Offset Ticket direction for that control. A growing visual vocabulary was now prompting questions about how the page should behave, as well as how its parts should look.

Appending articles made the earlier palette decision operationally important. The Offset Ticket implementation fetches the next same-origin HTML page, checks its sequence and article identifiers, avoids duplicate posts, and assigns the appended title cards colours from the current page. It then recalculates the connected wall’s edge classes and announces the added posts to the hover-preview code. Incoming content therefore joins the existing composition. A request that sounded like changing pagination also required preserving the page’s visual relationships as its contents grew. If fetching fails, the reader is offered an ordinary link to the next page. Those are properties of the supplied code; the conversation’s local checks used simulated requests rather than establishing every live browsing condition.

The code makes the extent of that reuse more precise. The pagination enhancement first captures the rendered colour pairs of the existing cards:

const colours = articles(main)
 .filter(n => n.matches('.yh-title-card'))
 .map(n => {
  const css = getComputedStyle(n);
  return ['--poster-bg','--poster-fg']
   .map(p => css.getPropertyValue(p).trim());
 })
 .filter(pair => pair.every(Boolean));

Inside the loop importing additional articles, it applies those pairs cyclically, using the number of title cards already present as the starting index:

if (post.matches('.yh-title-card') && colours.length) {
 const [bg,fg] = colours[index++ % colours.length];

 [
  ['--poster-bg',bg],
  ['--poster-fg',fg],
  ['--poster-hover-bg',fg],
  ['--poster-hover-fg',bg]
 ].forEach(([key,value]) => post.style.setProperty(key,value));
}

This preserves the current colour family, but it does not rerun the recursive palette search for every appended group. Repeating the captured sequence therefore should not be assumed to preserve every neighbour constraint at its wraparound boundary. That is a distinction revealed by source inspection, not a claim that I observed a visible failure. Likewise, joinWall() recalculates left, right, top and bottom classes, including a full-width last card for an odd-length run; the yin:posts-added event lets the image-preview enhancement respond to new content. Colour continuity, border continuity and interaction continuity have separate mechanisms.

Consistency did not require giving everything equal force. The welcome section made this particularly clear. I first considered highlighting my interests and activities, then preferred plain text because the extra colour was excessive. When its second sentence looked weaker than the first, I asked for a continuous statement with a consistent typographic weight. That did not settle the greeting. After I added “Dobrodošli!” beside “Benvenuti!”, a screenshot showed it falling into the ordinary body text; the correction then made both greetings too monumental. I asked to retain the size of “Benvenuti!” while reducing “(Dobrodošli!)”, and eventually chose the smaller serif aside. The successive corrections established different kinds of equality: the biographical sentences should speak with the same weight, while the two greetings could relate through a deliberate difference. A coherent system needed distinctions of scale and role as much as repeated colours and borders.

There were decisions about wording inside that typographic work. I changed “AI, computational theology, and digital humanities” to “AI, computing, theology, and digital humanities”, giving computing and theology their own places in the sentence. The continuation named web development, endurance cycling, photography since 2009 and being an Oxford Clarendon Scholar. That was the material I objected to seeing relegated to a weaker style. I also specified that I meant Nanasu when using “Dobrodošli!”, rather than presenting the greeting as standard Croatian. This was how I wanted the wording understood in my introduction. Its parentheses, exclamation mark and relation to “Benvenuti!” then became concrete design questions. The revisions were therefore about presenting chosen words accurately as well as finding attractive typography for them.

The punctuation problem had a precise cause. The greeting parser recognised the parenthesised word without an exclamation mark; adding one left it in the surrounding body text. The repair changed that literal expectation:

needle = r'\(Dobrodošli\)'
fixed = r'\(Dobrodošli!?\)'

Making the exclamation mark optional repaired the structural recognition. Choosing the smaller serif aside remained an independent aesthetic decision. The two stages should not collapse into a single claim that the greeting had been “fixed”: one made the intended element available to styling, while the other established the relationship I wanted between the greetings. Ordinary editing had exposed how tightly presentation depended on the wording of the introduction.

Turning a repeated image into a sequence of identities

The moving logo began from something already present on the site: repeated copies of my existing gold logo travelling from left to right. I liked the movement and wanted to preserve the unusual hover behaviour, which could sometimes accelerate it and sometimes stop it. What I began to question was the repetition of exactly the same image.

I brought the footer marquee on Signal-A Studio’s website into the conversation as a visual reference, including a screenshot and its markup. What interested me was the sequence of differently drawn versions of a name, with a smaller identifying mark alongside them. I asked whether my own logo could become one element in a similar sequence, alternating with “YIN Renlong” in different typefaces. The separate gold V could appear on its own and as a small superscript-like mark beside the lettering (Signal-A Studio, n.d.).

The reference made a visual relationship available before it supplied any answer about generation. The markup I shared used repeated image elements for the marquee. My request gradually went further: could the sequence contain different fonts, then Chinese lettering, then a fresh combination on another visit? The reference helped me recognise an arrangement I liked; subsequent questions turned that arrangement into a different technical problem. It would be misleading to describe the finished generator as something already contained in the initial reference.

The original logo therefore retained an important place. I was not commissioning a replacement symbol and discarding the previous one. I wanted the familiar image to move among other ways of writing the same name. The small V gave the sequence another recurring element. A heavy condensed name, an italic serif name and a Chinese name could look quite different while sharing that mark, the current palette and the same direction of movement. The original image and the V also retained their gold colour as the surrounding palette changed. Looking back, that exception helped clarify what I meant by coherence: a recognisable element could keep its own colour and still belong within a changing composition.

I then asked to include 尹仁龙. Later, I explicitly added the traditional form 尹仁龍 and the individual characters 龙 and 龍. These choices expanded the visual vocabulary without requiring unrelated words. The sequence could alternate between a full name and a single character, between Latin lettering and Chinese characters, and between the original image and live text. The content remained tightly defined even as its presentation became more varied.

The offset-print treatments were especially appealing to me. A slight displacement, a cut shadow, a reversed block or an incomplete frame could make the lettering feel less like a conventional website label. Yet this preference also required restraint. Some combinations became too heavy or too decorative. The idea that eventually interested me was a generator capable of combining type and treatment, with rules about which combinations were worth allowing.

The practical corrections continued alongside that larger idea. I asked for a height of 100 pixels, removal of the visible “New mix” and “Pause motion” buttons, and alignment with the actual page container. My screenshot of the browser’s box model showed why an approximate viewport-width solution was inadequate: the page had substantial internal padding, including 108 pixels on each side at the inspected desktop size. The strip needed to meet the page’s own edges, including that padded area, and then sit flush against its top. An attractive moving composition still looked wrong when it appeared detached from the frame containing the rest of the site.

Those measurements also changed the meaning of “full width”. The earlier CSS used 100vw, which referred to the viewport. What I wanted here was the width of the #page container, including its padding. The implementation consequently needed to work from that container’s geometry rather than treat a desktop measurement as a universal width. Once the horizontal relationship was acceptable, the small gap at the top became the next visible problem. Alignment was being discovered through inspection of the assembled page, not resolved by declaring the marquee full-width in isolation.

The architecture of the current implementation

The implementation needs to be visible for this account to explain what was actually built. The source examined here is the newest prepared version available in this work: the main logo plugin remains at version 2.0.0, its catalogue is extended to 63 faces by the Japanese font pack, and the miniature-footer adapter is version 1.0.0. The excerpts below come from that source and its preparation and checking scripts. They are parts of the existing implementation, with surrounding code omitted where unnecessary; they are not a new installation patch or evidence of a subsequently confirmed live deployment. The main plugin was also recovered from its packaged installer and checked against the embedded source checksum. All listings are excerpts from the project’s source, preparation scripts or recorded checks, with publication escaping where needed; they are not standalone installation instructions. The font collector and outline-comparison scripts run during preparation. The production installer downloads their prepared assets rather than running font subsetting on the server (Yin with ChatGPT, 2026).

There are three different stages. During preparation, Python obtains and checks the small font files and produces metadata and stylesheets. On a WordPress request, PHP supplies the catalogue, styling and JavaScript. In the browser, JavaScript selects an edition, loads its selected faces, builds and measures the sequence, and animates it. The footer then derives its miniature from that rendered header. Network access to the font catalogue services belongs to preparation; visitors receive the prepared font assets from my website. This division is reconstructed from the accumulated implementation. It was not an initial blueprint. Theme edits and purpose-specific must-use plugins grew around an existing site, which helps explain both their useful boundaries and their remaining integration compromises.

Component Responsibility Output or dependency
yin-home-connected-grid.php Build the connected title wall and choose page-palette editions. Produces #yin-poster-palette and stores the chosen family.
yin-navigation-connected.php Provide connected navigation and bridge the page palette to other components. Publishes root --ycn-* properties.
yin-corner-stamp-footer.php Provide footer text, background information and the existing name link. The miniature enhances its a.yfc-name.
Font preparation scripts Request subsets, inspect character maps, retain licences, hash files and compare outlines. Checked WOFF2 files, catalogue records and fonts.css.
yin-bilingual-logo.php Supply the main catalogue, CSS and browser generator. The header edition under #masthead .yig-shell.
Japanese font extension Append eligible records and enqueue its additional font stylesheet. The existing generator gains more material; its composition algorithm remains shared.
Page palette Expose paper, ink and accent colours through --ycn-* CSS properties. The logo’s --yib-* properties inherit those values.
Browser edition generator Combine checked fonts, text, patterns and treatments using recent local history. One nine-element sequence and enough copies to fill the moving strip.
yin-mini-footer-logo.php Copy the header sequence and rescope its stylesheet; manage the miniature’s dimensions and movement. A smaller rendering inside the existing footer name link.

The two PHP plugins live in WordPress’s must-use plugin directory, located through WPMU_PLUGIN_DIR. WordPress loads these plugins automatically; they are not enabled through an ordinary activation click (WordPress, 2026). The main plugin nevertheless restricts its output to the front end using the Penscratch theme. These are the actual enablement and font-loading methods from Yin_Bilingual_Logo_V1:

public static function enabled() {
 return !is_admin() && !is_feed() && get_stylesheet() === 'penscratch';
}
public static function fonts() {
 if (!self::enabled()) return;
 wp_enqueue_style('yin-logo-press-fonts', plugins_url('yin-logo-press-v2-b1d0113d199b/fonts.css', __FILE__), [], self::VERSION);
 // YIN_JAPANESE_FONT_PACK_V1
 wp_enqueue_style('yin-logo-japanese-fonts', plugins_url('yin-logo-japanese-v1-198d0456d00b/fonts.css', __FILE__), [], '1.0.0');
}

The asset-directory suffixes identify the prepared asset sets. They are generated values in this version, rather than names that every future font pack must retain. The WordPress registrations connect those methods and the CSS and JavaScript emitters to the page lifecycle:

add_action('wp_enqueue_scripts', ['Yin_Bilingual_Logo_V1','fonts'], 30);
add_action('wp_head', ['Yin_Bilingual_Logo_V1','styles'], PHP_INT_MAX);
add_action('wp_footer', ['Yin_Bilingual_Logo_V1','script'], PHP_INT_MAX);

This architecture also explains where a modification belongs. A new admissible face changes the catalogue and its font assets. A new treatment changes the generator’s allowed recipes and the CSS that draws them. A different page palette changes the shared colour properties. The footer’s adapter needs a change when its own scale or interaction changes, or when the header interface it consumes changes. These boundaries are practical properties of this implementation, rather than a claim that every website should use the same plugin arrangement.

One architectural qualification emerged only on reading the palette bridge. Both the homepage and navigation contain definitions of the eight families. The bridge recognises the selected family by looking for its colours in the generated homepage CSS, or consults session storage when that stylesheet is absent, and then publishes the shared variables:

function sync() {
        const source = document.getElementById('yin-poster-palette');
        const css = source
            ? source.textContent.toLowerCase().replace(/\s+/g,'')
            : '';

        let family = families.find(p =>
            [1,3,5,7].some(i =>
                css.includes('--poster-bg:' + p[i] + ';')
            )
        );

        if (!family && !source) {
            try {
                family = families.find(p =>
                    p[0] === sessionStorage.getItem(
                        'yin-poster-palette-family-v1'
                    )
                );
            } catch (_) {}
        }

        family = family || families[0];

        names.forEach((name,i) => {
            document.documentElement.style.setProperty(
                '--ycn-' + name,
                family[i+1]
            );
        });
    }

The output is coordinated, but its family definitions are duplicated. Adding or changing a family requires keeping those definitions aligned. An explicit shared palette record could replace recognition through CSS text; that remains a possible refactoring. This makes the later footer question more exact. “One source” can be true of one relationship in the site and remain incomplete in another. The appearance of a coherent system cannot by itself establish how its components are maintained.

Giving the generator a vocabulary and a grammar

At this point I asked whether the combinations could be generated programmatically. There were many freely available fonts, and I wanted a refresh to have a good chance of showing something unfamiliar. AI helped turn that request into a more specific architecture: a curated font catalogue, a set of typographic treatments, a small collection of sequence patterns, and rules governing how they could be combined.

The transition did not happen with the first animated prototype. I already liked its title-logo style when I said that it still seemed to have a numerical limitation. I asked for more styles and combinations, and specifically wondered whether an algorithm could mix typefaces with design elements. I also asked for insights before another patch. At that stage, another handful of examples would have extended the list without answering the new question: what would produce the examples, and what would keep the results related?

The assistant identified a specific limitation in that prototype: positions and familiar pairings kept returning, including Chinese cut shadows, framed serif lettering and bold Latin names. Its useful formulation was to “separate the typeface from the treatment”. A serif could take a fine cut on one visit, a reversed panel on another, or an open frame. That explanation gave my dissatisfaction a mechanism to address. This proposal came before the four-character discussion; the resource discovery subsequently made a broader approved vocabulary practical. Their sequence matters because a smaller download answered the cost of the generator I was beginning to want.

The distinction between a font catalogue and a live search for fonts is important. The implementation does not discover an arbitrary new typeface somewhere on the internet every time a visitor opens the page. Fonts are selected and checked in advance, their small files are hosted on my site, and the browser composes an edition from that approved collection. The letterforms remain the work of their type designers; our generator arranges and treats that existing material. Expanding the collection is a separate maintenance operation. The variation happens automatically; the admission of new materials remains a decision. Automatic catalogue discovery and variation along font axes were discussed as future possibilities. Neither became part of this delivered generator, which uses prepared weights and styles. That boundary keeps my wish for something new on every refresh distinct from the finite mechanism that was actually supplied.

The first expanded catalogue contained 47 font faces: 27 Latin and 20 for Chinese text. A generated edition selected six distinct faces, four Latin and two Chinese, rather than loading the entire catalogue. The Latin selections covered different groups, including condensed lettering, serif or italic forms, sans or slab forms, and monospaced or display lettering. Both 尹仁龙 and 尹仁龍 appeared, together with a standalone dragon character drawn from one of those selected Chinese faces. The original logo and the separate V were incorporated into the sequence.

Six selected faces did not mean that the strip contained only six objects. The prepared sequence had nine elements: four Latin name treatments, two Chinese names, a single dragon character, the original logo and the separate mark. Reusing a selected face for the single character added another scale and kind of element without requiring another font. This is a small example of how the breadth of the visible composition and the number of resources it needed could be considered separately.

There are smaller ordering decisions inside those totals. The four Latin strings allow different capitalisation; italic faces use only the two mixed-case forms. A shuffle changes the order of the four selected Latin faces, and a separate draw can reverse the two Chinese names. The standalone character then reuses one of those Chinese selections. This original excerpt shows those operations together:

ls.forEach(w=>{w.text=w.face.group==='italic'?pick(latinText.slice(1,3)):pick(latinText);});
for(let i=ls.length-1;i>0;i--){const j=Math.floor(random()*(i+1));[ls[i],ls[j]]=[ls[j],ls[i]];}
const cs=['尹仁龙','尹仁龍'].map(text=>({kind:'word',text,face:face(fonts.filter(f=>f.script==='zh'&&Array.from(text).every(c=>f.chars.includes(c))))}));
if(random()<.5)cs.reverse();
const glyphSource=pick(cs);
const glyph={kind:'word',text:glyphSource.text.slice(-1),face:glyphSource.face};

Here latinText contains YIN RENLONG, YIN Renlong, Yin Renlong and yin renlong. Case, order and script are therefore distinct sources of variation, each attached to a limited choice. The short vocabulary is doing practical work throughout the composition algorithm, not only reducing the font files.

The catalogue and the selection function

The runtime catalogue contains capabilities, not only font names. These three records are taken directly from the 63-face array. script classifies the text for which the generator uses a face; zh does not assert that the font was designed in China. The chars field records actual available characters, including the space present in these subsets. The records describe the coverage established during preparation; the selection function itself trusts that metadata. They also identify prepared styles and weights: Cormorant Garamond uses italic at weight 600, whereas Shippori Mincho B1 uses normal style at weight 800 and lists both dragon forms. A family name alone would not specify these capabilities. The three records are:

[
  {
    "id": "anton",
    "script": "latin",
    "group": "condensed",
    "weight": 400,
    "style": "normal",
    "chars": " EGILNORYegilnory"
  },
  {
    "id": "delagothicone",
    "script": "zh",
    "group": "display",
    "weight": 400,
    "style": "normal",
    "chars": " 仁尹龍"
  },
  {
    "id": "shipporimincho",
    "script": "zh",
    "group": "serif",
    "weight": 800,
    "style": "normal",
    "chars": " 仁尹龍龙"
  }
]

Within compose(history, seed), the following helper excludes faces already used in the current edition. It scores the remaining candidates by their presence in recent editions, with more recent occurrences carrying greater weight, then chooses randomly among the least-used candidates. The subsequent group selection ensures that the four Latin faces perform different typographic roles:

const used=new Set();
function face(pool){
 const choices=pool.filter(f=>!used.has(f.id));
 const score=f=>history.reduce((n,h,i)=>n+(h.ids.includes(f.id)?i+1:0),0);
 const min=Math.min(...choices.map(score));
 const result=pick(choices.filter(f=>score(f)===min));used.add(result.id);return result;
}
const latinGroups=[['condensed'],['italic','serif'],['sans','slab'],['mono','display']];
const ls=latinGroups.map(groups=>({kind:'word',face:face(fonts.filter(f=>f.script==='latin'&&groups.includes(f.group)))}));

The face selector can also be stated precisely. Let \(P\) be the pool supplied for the current role, \(U\) the faces already selected for this edition, and \(H_1,\ldots,H_m\) the sets of font identifiers in the retained editions, ordered from oldest to newest. The code chooses among the eligible faces with the smallest weighted recent-use score:

\[
\begin{aligned}
E &= P\setminus U,\\
S(f) &= \sum_{i=1}^{m} i\,\mathbf{1}[f\in H_i],\\
A &= \operatorname*{arg\,min}_{f\in E}S(f),\\
f^{*} &\in A.
\end{aligned}
\]

The seeded random draw resolves ties within \(A\). For Chinese text \(x\), the supplied pool additionally requires \(\operatorname{chars}(x)\subseteq C(f)\), where \(C(f)\) is the recorded coverage of the face. Latin pools instead come from the complementary groups shown in the code, with their permitted letters checked during preparation. This is a sequence of conditional choices, so multiplying a catalogue size by a recipe count would overstate the number of admissible compositions. The function also assumes \(E\) is non-empty. A future catalogue edit must preserve that precondition; the current selector has no separate backtracking search to repair an exhausted font pool.

Those choices were only the vocabulary. The composition rules supplied the grammar. Strong coloured or reversed panels were limited to two and could not sit next to one another. Brush-like faces were kept free of additional text effects. Echo and split effects were excluded from Chinese lettering, where they could make an already complex character harder to read. An italic face was treated differently from a condensed display face. Small changes in spacing, scale and shadow displacement could vary the result without turning every element into a competing demonstration.

This was a useful way of making my earlier objections operational. Asking for more creativity had produced alternatives; explaining why a particular result was too much helped determine what the generator should avoid. The rules do not encode my entire taste. They capture a limited set of judgments that had become clear enough to express in software. The remaining judgment still happens when I look at the page.

I did not personally specify every numerical boundary in that grammar. I had not arrived with a rule permitting exactly two strong panels. The assistant translated the design direction into such constraints, and those constraints became provisional implementation choices that could be checked and revised. My agency was in changing the objective and judging its interpretations; the assistant also made substantive proposals about how to realise it. Recognising both contributions avoids giving either participant a misleadingly complete account of the work. A refusal could also redefine the next proposal’s objective, as when “more colours” became “one coherent palette”. My contribution therefore extended beyond approving an implementation, while the assistant’s contribution extended beyond mechanically translating a complete brief. The resulting rules remain interpretations that either of us could have formulated inadequately.

The restrictions are explicit in the implementation. The recipe list pairs a frame with a text effect; it offers fourteen combinations before the eligibility rules are applied:

const recipes = [['none','clean'],
  ['none','cut'],
  ['none','register'],
  ['none','echo'],
  ['none','split'],
  ['reverse','clean'],
  ['reverse','cut'],
  ['accent','clean'],
  ['accent','cut'],
  ['corners','clean'],
  ['corners','register'],
  ['open','clean'],
  ['open','cut'],
  ['rule','clean']];

The following excerpt sits inside the construction of an individual word element; panels, previousPanel, special and cutUsed track what has already been placed in that edition. The panel() helper recognises reverse and accent frames:

const group=word.face.group,zh=word.face.script==='zh';
let allowed=recipes.filter(([frame,effect])=>{
 if(panel(frame)&&(panels>=2||previousPanel))return false;
 if(['echo','split'].includes(effect)&&(zh||special>=1||group==='italic'||group==='display'))return false;
 if(group==='brush'&&effect!=='clean')return false;
 if(group==='italic'&&!['clean','register'].includes(effect))return false;
 return true;
});
let recipe=pick(allowed);
if(!cutUsed&&group==='condensed')recipe=['none','cut'];
const [frame,effect]=recipe;
if(panel(frame))panels++;
if(['echo','split'].includes(effect))special++;
if(effect==='cut')cutUsed=true;
previousPanel=panel(frame);

The final choice is therefore conditional on the face and the preceding composition. A condensed face can establish a cut treatment, while brush lettering remains clean. The code makes those judgments inspectable and changeable. It also exposes their limits: a rule about adjacent panels or an excluded effect is only an approximation of a visual judgment. It cannot determine whether every permitted combination is convincing. The smaller variations are bounded too: group-dependent base sizes receive a multiplier between 0.97 and 1.03, framed words are reduced by a factor of 0.88, and tracking and shadow offsets vary within limited ranges. These details allow difference within a chosen role rather than treating every property as an unrelated random choice.

The actual metric ranges are short enough to inspect directly. Chinese full names begin at 54 pixels and the single character at 62; the Latin base size depends on its group. The return value also records letter spacing, cut-shadow step, registration displacement and frame padding:

const sizes={condensed:70,sans:53,slab:51,serif:57,italic:60,mono:41,display:51,brush:58};
let size=zh?(word.text.length===1?62:54):sizes[group]||54;
if(frame!=='none')size*=.88;
return {...word,frame,effect,size:Math.round(size*(.97+random()*.06)),
 spacing:pick(zh?[-.02,0,.025]:[-.035,-.02,0]),step:pick([1.25,1.5,1.75]),
 dx:pick([2,3,-2]),dy:pick([2,3]),pad:pick([9,12,14])};

These values form a proposed typographic policy. For example, a monospaced Latin name begins smaller than a condensed one, while framed words receive less space than unframed words. They are not measurements demonstrating equal optical weight across all faces. The later browser measurement is still needed because the catalogue’s group label cannot predict the exact dimensions of every rendered word.

The generator also retained a short history of recent editions in the browser. It favoured faces used less recently, avoided repeating the immediately previous sequence pattern, and checked proposed combinations against recent signatures. That makes repetition less likely within the recent browsing history. It does not make the system infinite, and it cannot promise that every refresh will be unprecedented. My original desire for something always new had to become a more defensible aim: enough perceptible variation to keep the sequence interesting, within a finite and recognisable repertoire. The remembered history covers eight recent editions in that browser; another browser, or cleared local storage, does not inherit that continuity.

Seeds, sequence patterns and recent history

The sequence itself is selected from three patterns. L means one of the chosen Latin names, C a Chinese name, and G the single dragon character. The original logo and standalone mark retain explicit positions. The immediately preceding pattern is excluded before the next one is selected:

const patterns=[['logo','L','C','L','G','L','C','L','mark'],['C','L','logo','L','C','L','G','L','mark'],['L','C','L','logo','L','G','L','C','mark']];
const available=patterns.map((p,i)=>({p,i})).filter(p=>p.i!==history[history.length-1]?.pattern);
const chosen=pick(available.length?available:patterns.map((p,i)=>({p,i})));

A seed drives the composition’s pseudorandom generator. The browser tries up to sixteen candidates against recent signatures; it does not search indefinitely for an unseen result. These are the actual seed generator and edition-selection function:

function generator(seed){let x=seed||1;return ()=>{x^=x<<13;x^=x>>>17;x^=x<<5;return (x>>>0)/4294967296;};}

function nextEdition(){
 const history=historyRead();let result;
 for(let attempt=0;attempt<16;attempt++){
  const bytes=new Uint32Array(1);
  if(window.crypto&&window.crypto.getRandomValues)window.crypto.getRandomValues(bytes);
  else bytes[0]=Math.floor(Math.random()*4294967296);
  result=compose(history,bytes[0]);
  if(!history.some(h=>h.signature===result.signature))break;
 }
 return {edition:result,history};
}

The signature records each item’s kind, font, text, frame, effect and tracking. Some finer measurements, such as shadow displacement, are not included. It is consequently a practical repetition check rather than a complete description of visual similarity. With the same catalogue, history and seed, the composition can be reconstructed, but that is not a promise of identical browser rasterisation. The history is written after the edition is built:

try{localStorage.setItem(HISTORY,JSON.stringify([...history,{ids:edition.ids,pattern:edition.pattern,signature:edition.signature}].slice(-8)));}catch(_){}

The two kinds of browser memory have different scopes. Palette selection uses sessionStorage under yin-poster-palette-family-v1; logo history uses localStorage under yin-logo-press-v2-history. Neither is a central record of every visitor’s experience. Guarded reads and writes allow composition to proceed if storage is unavailable. The recent-use score also weights the later entries in that eight-edition history more heavily. It encourages less recently used eligible faces without promising equal selection frequencies across differently sized groups.

The AI participation in this system is also specific. AI helped propose the catalogue structure, write the generator and develop its checks. The browser then runs ordinary code. It is not asking a language model to redesign my name on every visit. Keeping that distinction clear matters because the visual result may look generative while the runtime mechanism remains relatively small, inspectable and predictable.

Drawing the treatments with shared colours

The generator chooses a frame and an effect, then writes classes such as frame-reverse and effect-cut. CSS draws the result. The first rule below bridges the existing page palette to the logo. The following rules show the fine cut, registration offset and two strong-panel treatments:

#masthead .yib-shell.yig-shell {
 --yib-paper:var(--ycn-paper,#f1efeb);--yib-ink:var(--ycn-ink,#292929);
 --yib-accent:var(--ycn-accent,#c4f542);--yib-accent-ink:var(--ycn-accent-ink,#292929);
 box-sizing:border-box;position:relative;left:var(--yib-page-shift,0px);top:var(--yib-page-top,0px);
 width:var(--yib-page-width,100%);max-width:none;margin:0;padding:0;text-align:left;
 color:var(--yib-ink);font-variant:normal;text-transform:none;direction:ltr;
}

#masthead .yig-shell .effect-cut .yig-type {
 text-shadow:var(--yig-step) var(--yig-step) 0 var(--yig-surface),
 calc(var(--yig-step)*2) calc(var(--yig-step)*2) 0 var(--yig-letter);
}
#masthead .yig-shell .effect-register .yig-type {text-shadow:var(--yig-dx) var(--yig-dy) 0 var(--yib-accent);}

#masthead .yig-shell .frame-reverse {
 --yig-surface:var(--yib-ink);--yig-letter:var(--yib-paper);
 padding:6px var(--yig-pad,12px) 8px;background:var(--yig-surface);
 box-shadow:4px 4px 0 var(--yib-accent);
}
#masthead .yig-shell .frame-accent {
 --yig-surface:var(--yib-accent);--yig-letter:var(--yib-accent-ink);
 padding:6px var(--yig-pad,12px) 8px;background:var(--yig-surface);
 box-shadow:4px 4px 0 var(--yib-ink);
}

The cut uses a surface-coloured shadow followed by a second, further-displaced letter-coloured shadow. Reversing a block changes the local surface and lettering variables, so the same cut can work on that different background. The accent block uses the palette’s corresponding accent-ink value. This is how the earlier preference for a unified palette reaches several effects without individually assigning colours to every generated word.

The rendering loop is the bridge between those data choices and their visible form. It handles the original logo and standalone V separately, creates a word element for each typographic item, attaches the small V, assigns the appropriate Chinese language label, chooses the successfully loaded face or a fallback stack, and writes the per-item CSS properties:

edition.items.forEach(item=>{
 if(item.kind==='logo'){const e=make('span','yib-original');e.append(image(originalImage.getAttribute('src'),270,84));run.append(e);return;}
 if(item.kind==='mark'){const e=make('span','yib-solo');e.append(image(markURL,75,75));run.append(e);return;}
 const f=item.face,e=make('span','yig-word frame-'+item.frame+' effect-'+item.effect);
 const type=make('span','yig-type',item.text);type.dataset.copy=item.text;e.append(type,image(markURL,75,75));
 if(f.script==='zh')e.lang=item.text.includes('龍')?'zh-Hant':'zh-Hans';
 const fallback=f.script==='zh'?(f.group==='sans'||f.group==='display'?'"PingFang SC","Microsoft YaHei",sans-serif':'"Songti SC","PMingLiU",serif'):(['serif','italic','slab'].includes(f.group)?'Georgia,serif':f.group==='mono'?'ui-monospace,monospace':'Impact,"Arial Narrow",sans-serif');
 e.style.fontFamily=(loaded.get(f.id)?'"YinPress-'+f.id+'",':'')+fallback;
 for(const [key,value] of Object.entries({size:item.size+'px',weight:f.weight,style:f.style,spacing:item.spacing+'em',step:item.step+'px',dx:item.dx+'px',dy:item.dy+'px',pad:item.pad+'px'}))e.style.setProperty('--yig-'+key,String(value));
 run.append(e);
});

The code also distinguishes occasions for change. It composes an edition during page initialisation and repeats that sequence as the strip moves; it does not choose fresh fonts every time a word passes across the screen. Hovering changes the movement, while refreshing can change the composition. This makes two different kinds of repetition visible. Recurrence within the moving strip gives the current edition its rhythm. Repeated editions across visits are what the recent-history mechanism tries to reduce. Asking for more randomness without distinguishing those timescales would leave the desired behaviour underspecified.

A closer look at the prepared code reveals another consequence of introducing different typefaces: the sequence no longer has a reliably fixed length. The earlier CSS specified a twelve-second belt duration. In the generator, selected fonts are given a bounded time to load, a suitable fallback is used when necessary, and the resulting sequence is measured. Its measured width determines both how many copies fill the strip and the duration needed to move at the normal speed of 24 pixels per second. A wider edition can therefore take longer to pass without automatically moving faster. The measurement concerns what has actually been rendered, including a fallback when a requested font has not loaded. Font choice, loading and movement are connected in the implementation, even though the original request expressed them as separate visual preferences.

Loading, measuring and moving an edition

The font-loading helper below gives each selected face up to four seconds. Its result controls whether the custom family is included in the element’s font stack. A failed or unavailable load therefore has an explicit fallback path; it does not prevent all other selected lettering from being assembled:

async function fontReady(face){
 if(!document.fonts)return false;
 let timer;
 try{return await Promise.race([
  document.fonts.load(face.style+' '+face.weight+' 60px "YinPress-'+face.id+'"',face.chars).then(xs=>xs.some(f=>f.status==='loaded')).catch(()=>false),
  new Promise(resolve=>{timer=setTimeout(()=>resolve(false),4000);})
 ]);}finally{clearTimeout(timer);}
}

The six selected loads are awaited together, and the original logo remains while preparation is pending. If a face times out, its custom family is excluded from that element’s stack. A late arrival therefore does not automatically replace the chosen fallback and change the measured geometry. Selection, successful loading and final rendering remain separate stages. The document.fonts interface supplies the document’s FontFaceSet; it does not itself decide this fallback policy (MDN contributors, n.d.).

The caller shows how that waiting is coordinated. It starts the selected loading operations through the mapping step, awaits their results together and records success by font identifier:

const {edition,history}=nextEdition();
const selected=fonts.filter(f=>edition.ids.includes(f.id));
const results=await Promise.all(selected.map(async f=>[f.id,await fontReady(f)]));
const loaded=new Map(results);
if(!old.isConnected)return;
shell.style.setProperty('--yig-gap',edition.gap+'px');shell.dataset.edition=String(edition.seed);

Promise.all() gathers the results in input order; the helper’s own error handling normally converts a failed face into a false result (MDN contributors, n.d.). The code does not deliberately wait four seconds six times in succession. Nor does a four-second timer establish a measured four-second page-load guarantee: scheduling, document state and rendering still matter. The old.isConnected check also prevents this asynchronous continuation from replacing a logo node that has disappeared while the fonts were pending.

The measurement function then duplicates the same rendered run, calculates the normal-speed duration, and preserves the animation’s current phase when its width must be recalculated. Moving from the negative run width to zero produces the requested left-to-right travel:

function measure(force=false){
 if(destroyed||!ready||!shell.isConnected)return;
 const width=run.getBoundingClientRect().width;if(!width)return;
 const count=Math.max(2,Math.ceil(belt.clientWidth/width)+1);
 while(track.children.length<count)track.append(run.cloneNode(true));
 while(track.children.length>count)track.lastElementChild.remove();
 if(!force&&Math.abs(width-runWidth)<.25)return;
 let phase=0;
 if(animation){const d=Number(animation.effect.getTiming().duration);if(d>0)phase=(Number(animation.currentTime||0)%d)/d;animation.cancel();animation=null;}
 runWidth=width;track.style.transform='translate3d(0,0,0)';
 if(!reduced.matches&&!print.matches&&track.animate){const duration=width/24*1000;animation=track.animate([{transform:'translate3d('+(-width)+'px,0,0)'},{transform:'translate3d(0,0,0)'}],{duration,iterations:Infinity,easing:'linear'});animation.currentTime=phase*duration;}
 motion();
}

Writing those relationships explicitly distinguishes speed from duration. For run width \(W\), visible belt width \(B\), copy count \(N\), and duration in milliseconds, the code implements:

\[
\begin{aligned}
N &= \max\!\left(2,\left\lceil\frac{B}{W}\right\rceil+1\right),\\
T_{\mathrm{header}} &= 1000\frac{W}{24}.
\end{aligned}
\]

The page alignment is likewise measured rather than hard-coded to the 108-pixel padding in my screenshot. This function derives the interior border-to-border width and adjusts the shell’s existing offsets to meet the page’s top and left edges:

function viewport(){
 if(!shell.isConnected||print.matches)return;
 const page=shell.closest('#page');if(!page)return;
 const p=page.getBoundingClientRect(),s=shell.getBoundingClientRect(),style=getComputedStyle(page);
 const left=parseFloat(style.borderLeftWidth)||0,right=parseFloat(style.borderRightWidth)||0,top=parseFloat(style.borderTopWidth)||0;
 const oldLeft=parseFloat(shell.style.getPropertyValue('--yib-page-shift'))||0,oldTop=parseFloat(shell.style.getPropertyValue('--yib-page-top'))||0;
 const set=(key,value)=>{const next=value.toFixed(3)+'px';if(shell.style.getPropertyValue(key)!==next)shell.style.setProperty(key,next);};
 set('--yib-page-width',Math.max(0,p.width-left-right));set('--yib-page-shift',oldLeft+p.left+left-s.left);set('--yib-page-top',oldTop+p.top+top-s.top);
}

The measured width spans content and padding inside the borders; the decorative outer outline adds no further content width. Including the existing offsets matters because moving the shell changes the rectangle returned on the next measurement. Resize observation schedules remeasurement through an animation frame, while preserving the animation phase avoids restarting the edition simply because the layout changed.

There is one more fitting step between constructing the words and starting the measured animation. After attaching the assembled shell, the generator inspects each word’s actual height. A word taller than 84 pixels receives a proportional font-size reduction towards an 82-pixel target:

old.replaceWith(shell);viewport();ready=true;
for(const item of run.querySelectorAll('.yig-word')){const h=item.getBoundingClientRect().height;if(h>84){const size=parseFloat(item.style.getPropertyValue('--yig-size'));item.style.setProperty('--yig-size',(size*82/h).toFixed(2)+'px');}}
measure(true);

This is a single correction pass. It does not repeatedly measure until every possible combination satisfies a proven height bound; fixed padding and borders need not shrink with the font size. That limit is relevant to the earlier distinction between valid combinations and convincing results. The code can reduce an oversized word without settling whether its final proportion, shadow or relation to its neighbours is visually right.

The motion controller keeps geometry separate from interaction state. Focus, reduced-motion preference, printing, a hidden document or an unready sequence take priority over the hover mode. The preserved hover interaction chooses a pause on 70% of eligible pointer entries and acceleration on the remaining 30%; acceleration uses a playback rate of 10.5. These are the implementation’s current values, not findings about what visitors prefer:

function motion(){
 const mode=focused||reduced.matches||print.matches||document.hidden||!ready?'paused':hover;
 shell.dataset.motion=mode;if(!animation)return;
 const rate=mode==='accelerated'?10.5:1;
 if(animation.updatePlaybackRate)animation.updatePlaybackRate(rate);else animation.playbackRate=rate;
 if(mode==='paused')animation.pause();else animation.play();
}

Changes in geometry and changes in interaction state follow different paths. The Web Animations API runs the moving track; a requested animation frame schedules occasional remeasurement after layout changes. Repeated scheduling cancels the previous pending callback, so the code does not intentionally rebuild the geometry for every resize notification:

function schedule(){if(frame)cancelAnimationFrame(frame);frame=requestAnimationFrame(()=>{frame=0;try{viewport();measure();}catch(e){recover(e);}});}

requestAnimationFrame() schedules a callback before a repaint; it is not the animation loop in this implementation (MDN contributors, n.d.). The main plugin observes the run and page with ResizeObserver when available, and also listens for resize and page-show events. Event-removal callbacks are kept so that failure recovery can cancel the animation, disconnect the observer, cancel the scheduled frame and restore the original link:

function recover(error){destroyed=true;if(animation)animation.cancel();if(observer)observer.disconnect();if(frame)cancelAnimationFrame(frame);removers.forEach(f=>f());if(shell.isConnected)shell.replaceWith(old);console.warn('YIN logo: original logo restored.',error);}

The recovery path is different from an individual font fallback. One unavailable face can still produce a generated edition using a system font; a broader exception in composition, rendering or scheduled measurement can abandon the enhancement and restore the original logo. Retaining the original node therefore supports both the waiting period and a return path after failure. This is visible in the implementation, rather than something established by its visual appearance.

The discovery that the Chinese logo needed only four characters

My first concern about expanding the Chinese typography was the size of the fonts. I suggested using existing or system fonts for the Chinese characters, while downloading the smaller Latin fonts onto the website. That was a reasonable concern to raise, but it treated the logo as though it needed the resources of an entire Chinese text environment.

The assistant’s decisive response was much narrower: “We only need four distinct characters”—尹, 仁, 龙 and 龍. That formulation changed what I understood the resource requirement to be. I had been considering the cost of adding a Chinese typeface in general; the assistant redirected attention to the characters this specific logo would display. I did not need every character in each typeface. I could use subsets containing the characters required by my name. I responded enthusiastically, explicitly agreeing that I needed only those four and asking for more Chinese font options on that basis. The new technical possibility changed my next request.

Character subsetting is an established technique, rather than an invention of this conversation. Google’s font API documentation explicitly describes requesting a known text string through the text= parameter, including for logos and headings. The service can then return a font file tailored to those characters (Google, n.d.). What was new for me was recognising how directly that technique fitted this design problem.

The assistant had also pointed out that the earlier logo code already requested 尹仁龙 with text=. The technique was therefore present before I recognised its wider implication. Extending it to 龍 and more faces changed what I believed the site could afford. My enthusiastic reply was directed at that newly understood freedom, not at discovering subsetting for the first time in the history of typography. FontTools documents the same general ability to subset a font by specified text or characters (fontTools developers, n.d.).

The exchange also separated storage from transfer. Hosting a font on my server does not remove the visitor’s initial download; it gives the site control over the prepared asset, which may then be reused from cache. System fonts avoid that additional file request but depend on what the visitor has installed. They remained fallbacks, while the small hosted subsets supplied the intended primary faces. My assumption that Latin fonts would necessarily be small also needed measurement rather than agreement. The relevant quantities were the prepared files and the faces selected for this edition.

The change in scale was substantial. The 47 prepared font files together occupied 120,376 bytes, approximately 118 KiB. That figure describes the font files, not the whole website or every associated asset. More importantly, the generator selected only six faces for an edition. The catalogue could be relatively broad without requiring each visitor to load all of it at once. The manifest total corresponds more precisely to 117.6 KiB and excludes CSS, licences, images and the rest of the page. It is a catalogue total, not a measured transfer total for a particular visit.

The preparation process checked the downloaded fonts themselves. It examined their character maps, recorded which of the required characters each face actually contained, retained their accompanying Open Font License files, and calculated checksums for the downloaded files. The browser could then select from known capabilities. Requesting a character in a font-service URL was not treated as sufficient evidence that the resulting typeface contained it.

Requesting and verifying the small font files

The collector requests the known logo text through the Google Fonts CSS API. In this original excerpt, family, weight and group come from a curated row, while CHINESE is 尹仁龙龍 and LATIN contains the name’s permitted letter cases. The collector’s fetch() helper performs the HTTP request; it runs during preparation, not on each visitor’s page load:

text = CHINESE if script == 'zh' else LATIN
italic = script == 'latin' and group == 'italic'
spec = family + (':ital,wght@1,' if italic else ':wght@') + str(weight)
query = 'https://fonts.googleapis.com/css2?' + urllib.parse.urlencode({'family':spec,'display':'swap','text':text})
css = fetch(query).decode()
urls = re.findall(r'url\((https://fonts\.gstatic\.com/[^)]+)\)\s+format\([\'"]woff2[\'"]\)', css)
if len(set(urls)) != 1:
    raise ValueError('Expected one WOFF2 character subset')
blob = fetch(urls[0])
if blob[:4] != b'wOF2' or len(blob) > 150000:
    raise ValueError('Unexpected font format or size')
font = TTFont(io.BytesIO(blob))
cmap = font.getBestCmap()
missing = set(map(ord,text)) - set(cmap)
if missing and script != 'zh':
    raise ValueError('Missing ' + ''.join(chr(x) for x in sorted(missing)))
if script == 'zh' and (not set(map(ord,'尹仁')).issubset(cmap)
                       or not set(map(ord,'龙龍')).intersection(cmap)):
    raise ValueError('No complete name in either writing form')
if script == 'zh' and set(cmap) - set(map(ord,CHINESE)) - {32}:
    raise ValueError('Not a four-character subset')

TTFont.getBestCmap() supplies a Unicode character map that can be inspected directly (fontTools, n.d.). The Chinese admission rule requires 尹 and 仁, plus at least one of 龙 or 龍. It also rejects mapped characters outside the requested set, allowing a space. A face supporting only the traditional name can therefore enter the catalogue without being falsely labelled suitable for the simplified name. The browser’s subsequent per-string filter performs the more specific eligibility check.

This introduced a second question after the first apparent solution. Subsetting answered how to avoid downloading unnecessary characters. It did not establish that every proposed font supplied all four necessary ones. A small file missing a character required by the selected name would still be inadequate for that name. The preparation therefore needed to preserve information about coverage, and the generator needed to use it. The font-size objection had opened the catalogue; checking the actual files determined which parts of that catalogue could be used for which text.

There is a small correction inside the assistant’s own account. The earlier proposal said each selected typeface would be checked for both 龙 and 龍. The collector instead admits a face that can supply either complete name, and leaves the stricter decision to the per-string filter. Reading the two together reveals a more flexible final rule than the initial verbal proposal. This does not establish a separate conversation in which I explicitly approved that refinement; it identifies the relationship between the proposal and the supplied code.

The preparation record also contains rejected attempts. Roboto Slab was recorded with an HTTP 404 and LXGW WenKai with an HTTP 400. Those messages establish failed retrieval attempts, without establishing that either family was aesthetically unsuitable or permanently unavailable. The collector logs such failures and admits successful, checked records. A broad catalogue was an outcome of preparation with exclusions, not the automatic inclusion of every name initially proposed. The distinction matters when considering the earlier possibility of automatic font discovery: discovery would still need an admission process.

This technical discovery changed the aesthetic question. Previously, Chinese typography had seemed likely to require a compromise for the sake of performance. Once the actual content was reduced to the four characters I needed, more variation became possible. The useful restriction was in the character set, while the diversity could move into the ways those characters were drawn. That relationship between a narrow vocabulary and a broad range of expression became one of the most satisfying parts of the project.

Trying Japanese typefaces without assuming they were automatically better

After expanding the catalogue, I asked whether Japanese fonts could also be used for the Chinese characters. I suggested, rather broadly, that some of the Chinese fonts looked less well designed than Japanese alternatives. That was an impression formed in this design process, not a comparative assessment of Chinese and Japanese type design. The assistant accepted the proposed exploration without endorsing the general ranking: individual faces should be judged for their suitability. This retained the productive part of my request while making the criterion more precise.

There were, however, specific Japanese designs that fitted what I was trying to achieve. Shippori Mincho B1 has rounded corners and an ink-pooling treatment, qualities that made it relevant to the print-like direction of the site. Dela Gothic One offers a very heavy, broad Gothic form, described by its project as suitable for posters and packaging. These were concrete relationships between a typeface’s construction and the visual effect I wanted (fontdasu, n.d.); (Dela Gothic project, n.d.).

Using Japanese-designed typefaces also required distinguishing characters from glyphs. Unicode describes regional conventions for the shapes of Han characters and explains that a character’s encoded identity does not specify one universal drawing. A font can therefore support the right character while presenting a form associated with a different typographic tradition (Unicode Consortium, n.d.). For this short, deliberately expressive logo, I was willing to explore such differences. That does not establish that the same choice would be appropriate for every Chinese paragraph elsewhere on the site.

The text itself remained 尹仁龙 or 尹仁龍. The addition of Japanese fonts did not mean translating the name or replacing 龍 with a different character such as 竜. The generated Chinese text retained its Chinese language labels. The experiment concerned the rendering of the requested characters, and the selection process had to respect the distinction between the simplified and traditional forms.

The checked Japanese additions made that distinction concrete. Sixteen faces were retained. All sixteen supported 尹仁龍, while four also supported the simplified 龙 required for 尹仁龙: Noto Sans JP, Noto Serif JP, Shippori Mincho and Shippori Mincho B1. The generator therefore filters the catalogue against the actual text before selecting a face. The relevant part of the implementation is small:

const cs = ['尹仁龙', '尹仁龍'].map(text => ({
  kind: 'word',
  text,
  face: face(fonts.filter(f =>
    f.script === 'zh' &&
    Array.from(text).every(c => f.chars.includes(c))
  ))
}));

The test is about the complete name being rendered. A face that supports 尹 and 仁 and the traditional 龍 is eligible for 尹仁龍; it is not automatically eligible for 尹仁龙. This prevents a random font choice from quietly relying on a different fallback face for the final character. A visually adventurous identity still needs the basic integrity of its text.

Another check revealed that increasing the number of font names would not necessarily increase the visible variety. For the three characters 尹仁龍, Kaisei Decol and Kaisei Opti produced identical outlines in the downloaded versions. The same was true of Yuji Mai and Yuji Syuku. One member of each pair was therefore omitted from this addition. That finding is limited to the characters and versions inspected; it does not claim that the complete typefaces are identical. For my logo, however, the distinction was exactly the relevant one. The conclusion comes from the recorded preparation and the surviving comparison script. It has not been turned into a newly repeated outline measurement in this revision.

The assistant’s preparation and outline checks thus complicated my original request for more options. The same four-character restriction that made many fonts economical also made differences elsewhere in those fonts irrelevant. Two families could differ across their wider character sets and still add no new drawing of my name. The relevant unit of variety had become the rendered characters in this application. That was a stronger criterion than the number of names in a font catalogue, and one that could not have been settled by enthusiasm for a larger selection alone.

The outline comparison also has concrete code behind it. The review script loads each collected Japanese subset, draws the three traditional-name glyphs into a recording pen, normalises coordinates and advance widths by units per em, rounds them to five decimal places, and hashes the resulting records. This is a comparison of these recorded forms at that precision, rather than a complete typographic equivalence test. FontTools documents the decomposing pen as recording component glyphs as contours, which explains why the script compares their drawn form rather than only retaining component references (fontTools developers, n.d.). The original comparison is:

groups={}
for f in c:
 font=TTFont(base/'fonts'/f['file']);glyphs=font.getGlyphSet();cmap=font.getBestCmap();em=font['head'].unitsPerEm
 outlines=[]
 for char in '尹仁龍':
  g=glyphs[cmap[ord(char)]];pen=DecomposingRecordingPen(glyphs);g.draw(pen)
  ops=[(name,[tuple(round(v/em,5) for v in point) if point is not None else None for point in pts]) for name,pts in pen.value]
  outlines.append((round(g.width/em,5),ops))
 key=hashlib.sha256(json.dumps(outlines).encode()).hexdigest()
 groups.setdefault(key,[]).append(f['family'])

The preparation record gives the sixteen additional subsets as 26,540 bytes, approximately 26 KiB, bringing the catalogue to 63 faces. The binaries were not downloaded again to remeasure that figure for this revision. They included heavy, rounded, serif and brush-like options, with the existing restrictions on effects still applied. The expansion increased the range of available material without changing the principle of selecting a small number of faces for any one edition.

The extension is additive. It parses the existing main plugin’s const fonts array and appends records whose identifiers are not already present. Conflicting settings for an existing identifier stop the patch. The following part of the installer changes that catalogue while retaining the surrounding generator:

by_id={f['id']:f for f in pool}
if len(by_id)!=len(pool):
    raise SystemExit('Duplicate existing font IDs. Nothing changed.')
for f in fonts:
    item=dict(id=f['id'],script='zh',group=f['group'],weight=f['weight'],
              style='normal',chars=f['chars'])
    if f['id'] in by_id and by_id[f['id']]!=item:
        raise SystemExit('Font already has different settings: '+f['id'])
    if f['id'] not in by_id: pool.append(item)
source=source[:start]+json.dumps(pool,ensure_ascii=False,separators=(',',':'))+source[start+length:]

A companion fonts.css supplies the new @font-face declarations, and the additional enqueue call shown earlier makes them available. The four-character subsets, the catalogue’s coverage metadata and the runtime filter therefore form a connected path. Adding a font name alone would leave that path incomplete.

The Japanese extension builds those declarations from the records. Its filenames include a digest prefix, and its Unicode ranges describe the characters that a declaration is intended to supply:

css=''
for f in fonts:
    f['file']=f['id']+'-'+f['sha256'][:12]+'.woff2'
    css+=('@font-face{font-family:"YinPress-'+f['id']+'";src:url("'+f['file']+
          '") format("woff2");font-weight:'+str(f['weight'])+
          ';font-style:normal;font-display:block;unicode-range:'+
          ','.join('U+%X'%ord(c) for c in f['chars'])+';}\n')

The declaration cannot manufacture a missing glyph. The metadata, the actual file and the renderer’s eligibility test each carry a different part of the claim that a name will appear correctly. A shorter download therefore solved one constraint while making this chain of checks more consequential.

One source for the header and the footer

The footer brought the inquiry back to maintenance. Its existing name link read “YIN Renlong / 尹仁龙”. I wanted a small moving version of the logo in that same space. I did not want to enlarge the footer to accommodate another dominant banner, and I did not want a second design catalogue that would gradually drift away from the first. The proposal estimated about 178 by 37 pixels; the implementation’s more durable choice was to derive its footprint from the existing name rather than treating that estimate as a universal fixed size.

My first request described the desired appearance and scale. The follow-up, before using the patch, asked about the relationship in the code. Two elements could look identical because they shared a source, or because someone had copied the same design into two independently maintained implementations. A screenshot could not establish which arrangement I was being given. The prospect of changing the main logo again made that distinction consequential: I wanted a shared source precisely because the design was still developing.

My follow-up question was therefore essential: was the footer really referring to the original logo, so that a later change could be made once? The answer was yes within the current implementation, with an important qualification about how the connection worked. The footer adapter waits for the header’s generated sequence, copies that rendered sequence, and copies the relevant stylesheet with its selectors scoped to the footer. It does not choose a second set of fonts.

Two operations in the adapter make that relationship visible. Here, source is the generated header sequence and css is the header’s logo stylesheet:

const run = source.cloneNode(true);

shared.textContent = css.textContent.replace(
  /#masthead/g,
  '#colophon.yfc-corner a.yfc-name.yfm-mini'
);

The copied sequence carries the chosen lettering, its inline font selections and its print treatments. The footer can use fonts already selected for the header. Its own adapter controls the scale and motion: it preserves the original name’s footprint, moves more slowly, and pauses on hover or keyboard focus. These are reasonable differences between a prominent header and a small footer element. Sharing the identity does not require sharing every dimension or interaction parameter.

The footer also keeps the original link destination and its accessible name. The repeated visual sequence is hidden from assistive technology, so the duplicated lettering does not become a succession of names for a screen reader to encounter. The header similarly keeps a single home-link label around its decorative sequence. These details matter because the logo is still part of a functioning interface. The generator can multiply its visible forms while the underlying link continues to have one purpose. This semantic arrangement is a separate matter from the unresolved question of controlling its motion. The adapter removes cloned IDs, hides both the sizing content and animated viewport from assistive technology, and maintains one accessible label on the link itself. These operations prevent visual repetition from multiplying its semantic identity.

This is a practical reuse mechanism with a defined boundary. The copying happens during page initialisation. Changes made through the existing main generator are reflected after a fresh page load. It is not a continuously synchronised component that automatically absorbs every later stylesheet mutation. If the generator’s markup or stylesheet identifiers are replaced, the adapter may need updating. Describing those limits makes the promise of changing the design once more useful, because I know which changes it actually covers.

The shared source also introduces a dependency in time. The footer cannot copy a sequence that the header has not yet generated, so the adapter briefly waits for the required elements. If they do not appear, the original static name remains. This fallback preserves the footer’s basic function without inventing another logo generator to compensate. It gives a concrete limit to the reuse promise: the miniature is an enhancement derived from an available header, with the existing name retained when that enhancement cannot be established.

Waiting for the source and keeping the existing footprint

The adapter does not assume that PHP output order guarantees a finished header: the main generator still has asynchronous font work to do. Its actual bootstrap checks for the footer link, generated run and shared stylesheet, and otherwise observes the document for up to twelve seconds:

function boot(){
 let pending=null,timer=null;
 function attempt(){
  const name=document.querySelector('#colophon.yfc-corner a.yfc-name');
  const source=document.querySelector('#masthead .yig-shell .yib-run');
  const css=document.getElementById('yin-bilingual-logo-css');
  if(!name||!source||!css)return false;
  if(name.classList.contains('yfm-mini'))return true;
  if(pending)pending.disconnect();clearTimeout(timer);
  enhance(name,source,css);return true;
 }
 if(attempt())return;
 pending=new MutationObserver(attempt);
 pending.observe(document.body,{childList:true,subtree:true});
 timer=setTimeout(()=>pending.disconnect(),12000);
}

The original name remains inside a hidden sizing span. Because that span still occupies layout space, the miniature retains the footprint of the original link rather than substituting a fixed width. An absolutely positioned viewport contains a 100-pixel-high stage, scaled to the available footer height:

#colophon .yfm-sizer {visibility:hidden;pointer-events:none;}
#colophon .yfm-viewport {
 position:absolute;inset:2px 0;display:block;overflow:hidden;pointer-events:none;
}
#colophon.yfc-corner a.yfc-name.yfm-mini .yfm-stage {
 position:absolute!important;left:0!important;top:50%!important;
 display:flex;align-items:center;width:max-content!important;height:100px!important;
 margin:0!important;padding:0!important;
 transform:translateY(-50%) scale(var(--yfm-scale,0.33));
 transform-origin:left center;pointer-events:none;
}

The footer measures in its own coordinate system. It divides the scaled run measurement by the scale factor to recover the stage width, creates enough copies for the small viewport, and calculates duration at twelve visible pixels per second. The full measurement function shows why simply copying the header’s animation duration would not express the desired slower miniature:

function measure(){
 if(dead)return;
 if(reduced.matches||printing.matches){motion();return;}
 const height=view.clientHeight;if(!height)return;
 const scale=height/100;
 stage.style.setProperty('--yfm-scale',String(scale));
 const width=run.getBoundingClientRect().width/scale;
 if(!width)return;
 const copies=Math.max(2,Math.ceil(view.clientWidth/(width*scale))+1);
 while(track.children.length<copies)track.append(run.cloneNode(true));
 while(track.children.length>copies)track.lastElementChild.remove();
 if(animation&&Math.abs(width-lastWidth)<.25&&Math.abs(scale-lastScale)<.001){motion();return;}
 let phase=0;
 if(animation){
  const duration=Number(animation.effect.getTiming().duration);
  phase=duration?((Number(animation.currentTime)||0)%duration)/duration:0;
  animation.cancel();
 }
 lastWidth=width;lastScale=scale;
 const duration=width*scale/12*1000;
 animation=track.animate([
  {transform:'translate3d('+(-width)+'px,0,0)'},
  {transform:'translate3d(0,0,0)'}
 ],{duration,iterations:Infinity,easing:'linear'});
 animation.currentTime=phase*duration;motion();
}

If the footer’s available height is \(H\) and its unscaled run width is \(W\), the scale and duration are:

\[
s=\frac{H}{100},\qquad
T_{\mathrm{footer}}=1000\frac{sW}{12}.
\]

The bounding rectangle already includes the scale, so dividing it by that scale recovers \(W\). The duration then uses the visible travel distance \(sW\). This is why a miniature can share its source with the header while moving at twelve visible pixels per second rather than the header’s normal twenty-four.

The phase preservation in both measurement functions has a useful formal reading. If the old animation has elapsed time \(t\) and duration \(T\), and a new measured width gives duration \(T’\), the code transfers the fraction of the cycle already traversed:

\[
\phi=\frac{t\bmod T}{T},\qquad t’=\phi T’.
\]

This preserves relative progress through the repeated sequence. It does not promise that every glyph remains at the identical screen coordinate after a resize; changed dimensions can still alter the geometry. The distinction is why preserving phase is useful without being a complete claim of visual continuity under every layout change.

For reduced motion and printing, the footer stylesheet hides the moving viewport and makes the original name visible again. Its JavaScript also pauses when the link is hovered or focused, the document is hidden, or the footer is outside the viewport. The fallback and the shared visual source solve different problems: one preserves a usable link, while the other avoids maintaining a second typographic catalogue.

The offscreen pause has a concrete source, separate from document.hidden:

if(window.IntersectionObserver){
 intersection=new IntersectionObserver(entries=>{visible=entries[0].isIntersecting;motion();});
 intersection.observe(name);
}

The observer tracks the footer’s intersection with the viewport, while document visibility concerns the page as a whole (MDN contributors, n.d.). Its callback updates the footer’s motion state; it does not recompute the edition. If this API is unavailable, the other pause conditions remain, but this particular offscreen test is absent. The supplied header does not use the same intersection observer, so the two components’ resource behaviour should not be described as identical merely because their typography is shared.

The Japanese font pack follows the same arrangement. It extends the main catalogue and adds the corresponding font stylesheet. Once the main generator selects one of those faces, the footer’s copy inherits that selection. There is no separate Japanese-font configuration to maintain for the small logo. The same decision about typography reaches both places through one source.

What the patches and tests could establish

The design process took place on an existing WordPress site using the Penscratch theme, with additions implemented through patches and custom plugin code. That context made reversibility important. I had already restored earlier designs when a technically successful change produced a result I did not want. A patch that could be installed, inspected and undone supported the way I was actually making decisions.

The different kinds of evidence used throughout the process were now easier to distinguish. Screenshots helped me assess scale, alignment and visual relationships. Source inspection answered questions about catalogue reuse, character eligibility and the timing of the footer copy. Installer output established that a particular operation or check had completed. None could stand in for all the others. The archive rollback had already demonstrated why a success message could coexist with a failed design decision; the footer question demonstrated why a convincing appearance could leave an architectural question unanswered.

I also asked for directly copyable code because uploading files through SSH was inconvenient, and later explicitly asked that the patch contain no Base64 payload. These were practical requirements, but they affected the relationship between the proposal and my control over it. Readable code made the operation available for inspection. The latest installer checked the expected generator, verified downloaded files, prepared the change, created a backup and supplied a rollback command. It also guarded against silently undoing newer edits.

The Japanese extension’s download helper restricts the expected HTTPS hosts, bounds the response size and checks the recorded SHA-256 digest:

def download(url,sha,limit):
    hosts=('fonts.gstatic.com','raw.githubusercontent.com')
    if urlparse(url).scheme!='https' or urlparse(url).hostname not in hosts:
        raise RuntimeError('Unexpected download source.')
    last=None
    for attempt in range(2):
        try:
            with urlopen(Request(url,headers={'User-Agent':'YIN-Font-Pack/1.0'}),timeout=25) as r:
                if urlparse(r.url).hostname not in hosts: raise RuntimeError('Unexpected redirect.')
                content=r.read(limit+1)
            if len(content)>limit or digest(content)!=sha:
                raise RuntimeError('Downloaded file failed verification.')
            return content
        except Exception as error: last=error
    raise last

Other checks inspect the WOFF2 signature and retain the accompanying Open Font License files. A matching digest establishes that the bytes match the prepared record; it does not establish typographic quality or independently authenticate whoever prepared that record. These checks answer different questions. Their separation is useful precisely because the asset that is technically intact might still be the wrong one for this name.

Staging the change and refusing an unsafe reversal

The current Japanese-font installer performs its compatibility checks and verifies the font files before reaching the following activation step. A backup and rollback script already exist at this point. It writes the proposed PHP to a temporary file in the target directory, checks syntax, verifies that the original plugin has not changed during preparation, and only then replaces it:

fd,name=tempfile.mkstemp(prefix='.yin-jp-logo-',suffix='.tmp',dir=mu);stage=Path(name)
with os.fdopen(fd,'wb') as f:f.write(data)
os.chown(stage,info.st_uid,info.st_gid);os.chmod(stage,stat.S_IMODE(info.st_mode))
subprocess.run(['php','-l',str(stage)],check=True)
if target.is_symlink() or target.read_bytes()!=old:
    raise RuntimeError('Logo changed during preparation. Nothing activated.')
if not folder.exists():os.rename(stage_dir,folder)
os.replace(stage,target);installed=True

The rollback script also checks that the currently installed file still matches the version it is authorised to undo. Its guard is small but consequential:

if hashlib.sha256(current).hexdigest()!=s['installed']:
 raise SystemExit('Newer edits detected. Nothing changed.')

A later manual edit therefore does not disappear merely because an older rollback script is executed. This is a narrower promise than a general version-control system, but it fits the actual experiments described earlier: an installation can be reversed while its relationship to subsequent work remains explicit. These excerpts document the safeguards; the full installer also contains the environment checks, downloads, ownership handling, post-load validation and cleanup on which they depend. The rollback also recognises when the previous content is already restored. After activation, the surrounding installer verifies that WordPress loads the extension and invokes rollback if that validation fails. Staging and replacement reduce failure risks, but they do not constitute one transaction covering every asset and plugin file.

The earlier preparation reported 1,200 generated combinations covering all 63 faces, with six distinct selected faces per edition and checks on panels and effects. The surviving scripts clarify that “coverage” in this composition test means agreement with each record’s chars field. Separate local checks simulated animation direction, hover behaviour, keyboard focus and reduced motion, while installation checks exercised failure recovery and rollback. The PHP source was parsed locally; the installer additionally runs PHP lint on the server before activation. These are recorded preparation checks, not new live-site observations made during this revision.

Testing the composition as data

The generator’s compose() function can be exercised independently of the browser DOM. The test harness exposes that function from the actual runtime source and runs the following loop. It checks the structure and coverage of each generated edition, carries the same eight-entry history forward, and records which faces have appeared:

let history=[],seen=new Set(),signatures=new Set();
for(let seed=1;seed<=1200;seed++){
 const e=api.compose(history,seed*96731);
 assert.equal(e.ids.length,6);assert.equal(new Set(e.ids).size,6);
 assert(!signatures.has(e.signature));signatures.add(e.signature);
 assert(e.items.some(w=>w.text==='尹仁龙'));assert(e.items.some(w=>w.text==='尹仁龍'));
 assert(e.items.some(w=>w.text==='龙'||w.text==='龍'));
 let panels=0,previousPanel=false;
 for(const w of e.items){
  if(w.face){assert([...w.text].every(c=>w.face.chars.includes(c)));seen.add(w.face.id);}
  const panel=['accent','reverse'].includes(w.frame);assert(!(panel&&previousPanel));
  if(panel)panels++;previousPanel=panel;
  if(w.face?.script==='zh')assert(!['echo','split'].includes(w.effect));
 }
 assert(panels<=2);
 if(history.length)assert(e.pattern!==history.at(-1).pattern);
 history.push({ids:e.ids,pattern:e.pattern,signature:e.signature});history=history.slice(-8);
}
assert.equal(seen.size,63);

The assertion against previously seen signatures applies to this tested sample; the runtime itself only compares with its bounded recent history. Likewise, the assertions about six selected font identifiers are supplemented by a separate DOM check of six explicit font-loading calls. Keeping these checks distinct prevents a test of the catalogue selection from being misreported as a complete measurement of network traffic or visitor experience. The DOM harness substitutes a font-loading function that reports success, fixed element rectangles and simulated animation objects. It can establish that six load calls are requested and that the relevant state changes occur in that model. It cannot establish that six real font files arrived, that real glyphs rasterised correctly, or that the resulting loop looked seamless in a browser.

A separate source-level check was performed while preparing the later technical draft. It recovered the packaged generator, checked its source digest and JavaScript syntax, added the Japanese records, and ran seeds 1 through 1,200 with the eight-edition history. This differs from the earlier harness above, which multiplies its seed counter by 96,731. The later result was:

{
  "editions": 1200,
  "catalogue": 63,
  "facesUsed": 63,
  "maxPanels": 2,
  "maxSpecial": 1,
  "minUsage": 37,
  "maxUsage": 200,
  "scope": "Composition only; source-derived catalogue metadata, not font binaries or browser layout"
}

That check also verified nine items, four Latin and two Chinese faces, the combined echo/split limit, brush and italic restrictions, and non-repetition of the immediately previous ordering pattern. All 63 faces were reachable in the sample; their usage ranged from 37 to 200, which should not be described as uniform sampling. Neither run exhausts every permitted composition. A mistaken chars record could pass the selection test while misdescribing a binary, so admission checks on the font files remain separate evidence. This revision preserves both checks and their different scopes without pretending to have rerun them.

Those results establish specific things. They support the consistency of the composition rules and the behaviour of the prepared installation process. They do not demonstrate that every possible combination is attractive, that every browser will render the faces identically, or that a small footer animation will remain legible on every display. Local DOM checks are also different from inspecting the installed result in a browser. At the point this account reaches, the latest Japanese font pack and miniature-footer patch have been prepared and checked; the conversation does not yet establish that I have installed and reviewed both on the live site.

There is a further distinction concerning AI. It produced code, described what the code was intended to do, and ran checks whose outputs could be examined. Those are different forms of contribution and evidence. A confident explanation of a layout could still be followed by a screenshot that made me reject it. A passed test could still accompany a design I found too large or too busy. My decisions about the site remained dependent on seeing and using the result.

The sequence is therefore better understood as a feedback process than as the execution of a settled brief. A request produced a candidate; the candidate exposed something my request had left unspecified; my response changed what the next version had to accomplish. Sometimes the correction concerned the implementation. Sometimes, as with the enlarged photographs and the increasingly random colours, it concerned what I had asked for. The benefit of AI’s ability to produce alternatives depended on keeping that distinction open. Faster production could otherwise multiply a mistaken objective just as efficiently as a useful one.

The limits I still need to decide

Looking back while writing this account also exposes an unresolved tension in the motion design. This was a finding of the later documentation check, rather than the reason I had originally made the design decision. I had wanted the visible controls removed, and I liked the header’s unpredictable response to hovering. The code respects reduced-motion preferences and includes pauses for focus and other states. However, the W3C’s explanation of WCAG 2.2 requires a way to pause, stop or hide non-essential automatic movement that lasts more than five seconds alongside other content. It also explains that stopping only while focus is held is insufficient, because the visitor must remain able to use the rest of the page (W3C, 2026).

That makes a persistent, accessible way to stop the movement a remaining design task. It could be visually modest, or the animation could be reconsidered so that it stops on its own. I should not treat removal of a prominent button as having settled the question of control. The pleasure I take in the marquee and a visitor’s need to read without movement both belong in the design brief.

This also brings me back to the early reset-button problem. I wanted that control to stay in a predictable place, yet later welcomed an unpredictable response to hovering over the logo. The difference in purpose partly explains those preferences, but it does not remove the tension: the logo is also a link, and hovering cannot be relied on as a stop command if it sometimes accelerates the movement. I have not decided here to abandon that playful effect. I can, however, distinguish it more clearly from a dependable means of control. The same interaction should not be described as providing both unless its behaviour supports both.

The smaller footer introduces a related aesthetic question. A face that looks distinctive in the header may lose its character when scaled down. A delicate shadow may become insignificant, while a dense Chinese character may need more space than the existing name link provides. Sharing the source solves duplication, but it does not prove that every treatment works equally well at both sizes. That requires the next round of visual inspection.

I also do not yet know where useful variation becomes background noise. The refresh history reduces immediate repetition, but it cannot tell me whether a visitor experiences the new edition as interesting, distracting or barely noticeable. A larger catalogue has value only when its differences survive the actual scale and context of the page. The discovery of identical glyph outlines in differently named fonts was a particularly clear warning against counting options as though the count itself were a design achievement.

There are specific maintenance questions alongside that visual uncertainty. Palette definitions could be consolidated into one explicit record. The footer’s selector-based copy could become a shared rendering interface if its structure changes substantially. Future catalogue updates could make binary-coverage checking a repeatable admission step, and a footer-specific eligibility rule might exclude treatments that lose too much when reduced. These are proposed next steps, not changes already made. Each follows from an identified boundary in the current implementation.

What has changed is the kind of request I can now make. I began by asking for more styles, more colours and more alternatives. I can now ask which features should remain stable, where variation is welcome, which combinations should be excluded, and how one decision can reach several parts of the site without being maintained twice. AI helped develop the possibilities and make those distinctions executable. My objections, preferences and reversals gave the work its direction.

I should also resist making this reconstruction more orderly than the work itself. The conversation moved back and forth between local corrections and broader possibilities; it did not advance through a predetermined sequence of design principles. Some of the connections become clearer because I am writing about them now. The screenshots, questions, code and reversals constrain that interpretation, but they do not make every later formulation an idea I already held at the time. The system remains provisional partly because the understanding expressed through it is still being revised. Asking for the code changed the evidence available to the article, and that evidence changed the article’s account of the system. I can now distinguish what I wanted during the redesign, what the assistant implemented, and what a later reading allowed me to understand. The archive helps preserve those distinctions only if I also resist treating its confident explanations as substitutes for the source or for my own inspection of the result.

I still want the name to surprise me when the page opens. I want to see 尹仁龙, 尹仁龍 or YIN Renlong drawn in a way that feels fresh, with the familiar V returning somewhere in the sequence. I also want the articles to remain the reason for being there. The next version will have to satisfy both desires in the space of an actual page. If it does too much, I already know that going back can be part of moving the design forward.

References

Dela Gothic project. (n.d.). Dela Gothic One project description. https://github.com/syakuzen/DelaGothic/blob/master/DESCRIPTION.en_us.html. Accessed 29 October 2026.

fontdasu. (n.d.). Shippori Mincho B1 project description. https://github.com/fontdasu/ShipporiMincho/blob/master/B1_DESCRIPTION.en_us.html. Accessed 29 October 2026.

Google. (n.d.). Get started with the Google Fonts API. Section “Optimizing your font requests”. https://developers.google.com/fonts/docs/getting_started. Accessed 29 October 2026.

Signal-A Studio. (n.d.). Signal-A Studio. Website used as a visual reference during the redesign. https://signal-a.studio/. Accessed 29 October 2026.

Unicode Consortium. (n.d.). Frequently asked questions: Chinese and Japanese. https://unicode.org/faq/han_cjk.html. Accessed 29 October 2026.

World Wide Web Consortium (W3C). (2026). Understanding Success Criterion 2.2.2: Pause, Stop, Hide. Updated 10 August 2026. https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html. Accessed 29 October 2026.

WordPress. (2026). Must-Use Plugins. Advanced Administration Handbook. Updated 14 October 2026. https://developer.wordpress.org/advanced-administration/plugins/mu-plugins/. Accessed 29 October 2026.

fontTools. (n.d.). ttFont: Read and write font contents. Documentation for TTFont, getBestCmap() and getGlyphSet(). https://fonttools.readthedocs.io/en/latest/ttLib/ttFont.html. Accessed 29 October 2026.

Yin, R., with ChatGPT. (2026). Check Tag Display – yin.roma.it CSS theme. Supplied design conversation, screenshots, installation output and successive article drafts. Primary source includes yin-logo-generative-fonts-patch.sh, yin-logo-japanese-font-pack-patch.sh, yin-mini-footer-logo-patch.sh, the homepage, navigation, pagination and greeting patches, collect-fonts.py, font-specimens.py and check.cjs. Source listings and recorded checks are cited as project materials rather than as evidence of confirmed live deployment.

MDN contributors. (n.d.). Document: fonts property. https://developer.mozilla.org/en-US/docs/Web/API/Document/fonts. Accessed 29 October 2026.

fontTools developers. (n.d.). subset: Generate subsets of fonts or optimize file sizes. https://fonttools.readthedocs.io/en/latest/subset/index.html. Accessed 29 October 2026.

fontTools developers. (n.d.). recordingPen. Documentation for RecordingPen and DecomposingRecordingPen. https://fonttools.readthedocs.io/en/latest/pens/recordingPen.html. Accessed 29 October 2026.

MDN contributors. (n.d.). Promise.all(). https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all. Accessed 29 October 2026.

MDN contributors. (n.d.). Window: requestAnimationFrame() method. https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame. Accessed 29 October 2026.

MDN contributors. (n.d.). Intersection Observer API. https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API. Accessed 29 October 2026.

Cite this article

Chicago 18 · Bibliography

YIN Renlong. “Building a Rule-Based Generative Design System in WordPress.” The personal webpage of YIN Renlong, October 29, 2026. https://www.yin.roma.it/2026/10/building-a-rule-based-generative-design-system-in-wordpress/.

BibTeX

@misc{YIN2026_4960,
  author = {{YIN}, {Renlong}},
  title = {{Building a Rule-Based Generative Design System in WordPress}},
  year = {2026},
  month = {10},
  day = {29},
  date = {2026-10-29},
  howpublished = {The personal webpage of YIN Renlong (blog)},
  url = {https://www.yin.roma.it/2026/10/building-a-rule-based-generative-design-system-in-wordpress/}
}