255 Commits
Author SHA1 Message Date
jinhojang6 df278cde49 feat(web): draw the search-shift chart as a diverging bar chart
The chart's bars did not match its numbers. The three rows were drawn as left-aligned fills on separate scales: +48% took 88.6% of its track and +23% took 56.2%, so a figure twice as large rendered only 1.6 times as long. The -51% row encoded nothing at all — a full-width hatch, whose length said 100%.

All three rows now share one axis with zero in the middle. The side a bar falls on carries the sign, its length carries the size, and the three are finally comparable: +48% renders 2.09x the +23% bar, matching the figures. It also puts the section's argument in the shape of the chart — one bar leaving, two arriving.

The axis runs to 60% each way rather than to the largest value, so the -51% bar stops short of the edge instead of filling it; a bar that runs out of track reads as clipped rather than as its own value. The bound lifts automatically if the data ever outgrows it.

Texts, legends, arrows and the palette are unchanged, except that the grey rail is dimmed: at full strength the half sitting on the empty side of zero read as a bar running the other way, which is the one thing a diverging chart must not do. The zero line carries the weight instead.

Each row is now a single signed number — `change: -51` — with the printed value, the arrow and the bar length all derived from it, so the number and the bar cannot drift apart. A test pins bar widths to the figures they carry.
2026-08-27 23:37:50 +09:00
jinhojang6 12e9989163 content(web): pluralise the first chart row label
"Search for the site that complied" becomes "Searches for…", matching the two rows below it.
2026-08-27 23:37:50 +09:00
jinhojang6 82b306e0a2 content(web): apply the third set of Chat Control copy revisions
- The first chart row reads "Search for the site that complied". The parenthetical it drops — "(mostly by blocking the state)" — also contradicted the copy above the chart, which now says that site complied with the age verification.
- Scopes the IEEE study to how age verification is deployed "across the web in US states".
2026-08-27 23:37:50 +09:00
jinhojang6 b186ab324f content(web): apply the second set of Chat Control copy revisions
- The first CTA card reads "Explore the Tech" instead of "Build the Parallel".
- The market leader "complied with the age verification" rather than blocking or pulling out.
- The measurement is described as comparing web search activity, dropping the counterfactual framing.
- The compliant site lost half its *search* traffic — the study only ever measured searches.
- Cuts "The proposal fails to help and crowds out what works", turns the second "Meanwhile" into "At the same time", and softens "penalty" to "sanction".
2026-08-27 23:37:50 +09:00
jinhojang6 1577c7c3e2 content(web): credit Rivest and Diffie as pioneers of public-key cryptography
"who invented the public-key cryptography that secures the modern internet" becomes "pioneers of" — the same claim without overstating either authorship.
2026-08-27 23:37:50 +09:00
jinhojang6 fce9a94bdb refactor(web): move the campaign page to /chatcontrol
Renames the route from /catch-no-one to /chatcontrol, the slug the page will ship on.

The rename runs through everything that referenced it: the route constant becomes `chatControl`, the directory and its contract test are renamed, the page component follows, and so do the typewriter's arming class and the global its inline script sets — leaving those on the old name would have been a stale reference to a page that no longer exists.

Verified against the static export: canonical, og:url, and the Article JSON-LD `url` and `@id` all resolve to logos.co/chatcontrol, the sitemap carries the new path and not the old one, and the arming class still matches the Tailwind variants that key off it.
2026-08-27 23:37:50 +09:00
jinhojang6 a85f2a54ea content(web): apply the Chat Control copy revisions
Nine edits from the review doc, mostly tightening claims to what the papers actually support:

- The scanner paragraph names the defeat mechanism — poisoning and false-positive attacks — instead of the pixel-altering example.
- The behaviour-shift study is attributed to the US and hedged to "may be", rather than asserting what the movement was.
- The study result carries its figures: a 51% fall for the complying site, 48.1% and 23.6% rises for the rival and for VPNs.
- The age-check finding becomes what the study found — data reaching "fourth parties" less visible to users — rather than the leak framing.
- Section 04 is now "The Logos approach: Private tech and civic resilience".
- Ross Anderson's claim about family violence is attributed to the paper.
- Adds the Lang et al. credit under the search-demand chart, in the same face the quote cards use.

Three passages are dropped: the "machinery doesn't stay in the box" paragraph, and the chokepoint paragraph with the pull quote and closing paragraph that followed it.

The source credit is now a shared `SourceNote`, so the chart and the quote cards cannot drift apart. `PullQuote` goes with the pull quote it existed for. The heading test reads its expectations off the copy rather than repeating it, so an editorial change no longer reads as a broken page.
2026-08-27 23:37:50 +09:00
jinhojang6 d90aa93bb0 refactor(web): describe the campaign page once, not twice
The structured data still carried the page's opening paragraph as its `description` while the meta tags carried the search copy, so a crawler could read two different descriptions of the same page.

Both now come from a single `SEO` constant, which moves next to the rest of the page's copy. A test pins the metadata and the Article JSON-LD to the same string so they cannot drift apart again.
2026-08-27 23:37:50 +09:00
jinhojang6 8cc5151c2b chore(web): use the supplied SEO description for the campaign page
Replaces the meta description with the copy the team wrote. 134 characters, so search results still show it whole, and it now leads on the trade-off rather than on the mechanism.

Feeds og:description and twitter:description as well, since they all come from the same constant.
2026-08-27 23:37:50 +09:00
jinhojang6 42bae7c238 feat(web): drop the section eyebrows and the exhibit labels
Removes the small numbered headers above each section heading — `[00 / The promise]` through `04 / The work` — and the bold `Exhibit 01`–`Exhibit 04` labels above the source credits. The credit lines themselves stay: each card still names the work it quotes.

`02 / Broken promise` goes with the rest. Keeping it would have left the sequence as a lone "02".

Knock-on cleanups:

- Reverts the Fira Code 700 weight. It was pulled in for the exhibit labels alone, and nothing on the page renders bold monospace any more.
- Drops the now-unused `eyebrow` and `label` fields, the eyebrow props on `SectionHeading` and the light-panel dim constant. `SectionHeading` is a heading again.
- The WCAG AA shortfall goes with the eyebrows — those were the 3.11:1 / 2.97:1 / 2.25:1 measurements. The one dimmed run left is the case-file note at 4.67:1, which passes.
2026-08-27 23:37:50 +09:00
jinhojang6 bceb169f0c fix(web): stop words jumping lines while a quote types itself in
A word could start at the end of one line and hop to the next mid-run. Only the typed prefix was in the DOM, so the browser re-broke the lines on every character: "with" fitted at the end of line one until "with e…" no longer did, and the whole word dropped.

The quote now always holds its full text — the untyped tail is merely invisible, which still occupies its space — so the line breaks are the finished ones from the first frame and no glyph ever moves. Replaying every character of all four cards at 375 and 1440 gives exactly one distinct card height and one distinct position per probe word, against three heights and nine positions before.

That also removes the copy that existed only to reserve height, and with it the overlay: the quote is a single in-flow paragraph again, so the cap-height trim reaches it directly rather than needing to be repeated on an out-of-flow copy. Padding stays 40/40 at desktop and 24/24 at mobile, and a finished quote still copies exactly once.
2026-08-27 23:37:50 +09:00
jinhojang6 858989c51c fix(web): trim the text boxes Figma trims, and fix the quote card padding
The quote cards read with more space above the text than below. `text-box-trim` is not inherited and does not reach out-of-flow descendants, so the copy that types itself in — absolutely positioned over the one reserving the height — kept the half-leading its in-flow twin had trimmed and sat about 6px low. Every motion-enabled visitor saw that copy, so the cards rendered roughly 46/27 instead of Figma's 40/40. All four now measure 40/40 at desktop and 24/24 at mobile, with the typed copy landing exactly on the reserved one.

Three more text boxes were missing the same trim, each measured against its Figma node:

- The hero headline used `min-h-[1em]` per line, inflating the block to 112px against Figma's 97. The guard exists so the animated copy does not collapse while a line is empty, and that copy is absolutely positioned, so it now applies there only. The block measures 96.88.
- The CTA card title had no trim, pushing its button 7.6px down. Title and button now sit at 195.05 and 259.94 against Figma's 195 and 260.
- The study sub-heading likewise: 18.98 against Figma's 19.

The hero highlight also tracked the CSS line box rather than the fixed em-tall rect Figma draws, so it sat high and left no yellow margin under the text. Anchoring it at `0.118em` puts it at 52.98–108.98 against Figma's 53–109.

Also from review: copying a quote yielded the text twice, once from the screen-reader copy — which matters on a page built to be cited. Guarded `isArmed()` so an empty class name cannot throw out of `classList.contains`, which the comment already claimed. Added the `image` the Article rich result needs.
2026-08-27 23:37:50 +09:00
jinhojang6 2a6f10a8a6 feat(web): add structured data and tighten SEO for the campaign page
The page had no schema.org data and a meta description roughly twice the length search results display.

- Emits an `Article` block carrying the page's subjects and, more usefully, all six case-file sources as `citation` entries, so the sourcing survives outside the rendered list. Emits a `BreadcrumbList` alongside it.
- Cuts the meta description from 281 to 157 characters and leads with the terms people actually search for — Chat Control, age checks.
- Adds page-level keywords instead of falling back to the generic site list.
- Sets `og:type` to `article`, matching the other campaign page rather than the site default of `website`.
- Wraps the page in `<article>`: it is one long sourced argument, not a set of unrelated panels.

The structured data lives beside the route rather than in `lib/structured-data.ts`, because those shared helpers arrive with the site-wide SEO work still in review and the citation list is campaign-specific either way. Worth folding together once that lands.

`datePublished` is deliberately absent — Article rich results want one, but a guessed date is worse than none. It needs the real launch date.

Verified against the static export: canonical, hreflang, both JSON-LD blocks parseable, one h1, six h2s, and the route in the sitemap.
2026-08-27 23:37:50 +09:00
jinhojang6 15d9d9e932 refactor(web): render the hero headline statically, keep the typewriter behind a prop
The typewriter belongs to the quote cards, so the hero headline renders outright again. The machinery is kept rather than deleted: `TypewriterHeadline` becomes `HeroHeadline` with a required `animate` prop, and `hero.tsx` holds a single `ANIMATE_HEADLINE` constant to flip it back on.

The prop is required and the component renamed because `TypewriterHeadline animate={false}` reads as a contradiction, and a required flag makes the intent visible at the only call site.

With `animate` off the component skips the two-copy structure entirely and emits a plain <h1> — no reserved copy, no overlay, no aria-label. That matters because the arming class still lands on <html> for the quote cards: had the hero kept rendering both copies, its static copy would have been hidden and the headline would have typed itself in anyway. The hook takes an `enabled` flag so no timers or observers run either.

`TypewriterArmingScript` moves from the hero to the page root, which is what it actually arms now.
2026-08-27 23:37:50 +09:00
jinhojang6 f15cdecb10 feat(web): type the quote cards in, not the section headings
The typewriter on scroll was meant for the quoted exhibit cards, not the section headings. Headings go back to plain <h2>; each of the four light-blue quote cards now types itself in when the card scrolls into view. The hero headline is unchanged.

Both visual copies of a quote are hidden from assistive tech, and `role="paragraph"` cannot carry an accessible name, so the quote reaches screen readers through a dedicated screen-reader-only copy.

Also picks up the Copilot review:

- The CTA cards are clickable and were missing `cursor-pointer`.
- CTA hrefs were looked up positionally with a non-null assertion, so a new card would have silently rendered an undefined href. Cards now carry a key and the lookup is an exhaustive Record.
- The typewriter tests asserted Tailwind class strings, which AGENTS.md rules out. The two copies are now tagged with `data-typewriter` and the tests read their text instead. The class-drift test is gone: the armed class name is read back out of the Tailwind variant that uses it, so the script and the CSS cannot drift apart in the first place.
- Recorded the eyebrow contrast sign-off next to the constant, with the measured ratios, so it stops being re-raised.
2026-08-27 23:37:50 +09:00
jinhojang6 7d6b7614c7 fix(web): restore the hero type scale and match Figma's CTA row
The last refactor moved the type scale off the hero <h1> and never passed it back in, so the headline rendered at body size. `Hero` now hands `text-h2` and the cap-height trim to `TypewriterHeadline`, the same way `SectionHeading` feeds `TypewriterHeading`.

Also closes the last two measured gaps against the design. The CTA cards are laid out as Figma has them, two fixed 460px cards running 960px from the left gutter rather than a pair stretched across the content width, and the case file closes on Figma's 46px spacer.

The card column chain had to move onto `min-[…]` throughout: named breakpoint variants are emitted after arbitrary ones in this Tailwind build, so `md:grid-cols-2` was beating the 1440 rule.

The headline's two copies are now blocks rather than inlines wrapping block-level lines.
2026-08-27 23:37:50 +09:00
jinhojang6 acef64d359 feat(web): replay the hero typewriter and type section headings on scroll
The hero headline was gated on a sessionStorage flag, so a refresh showed it already complete. That gate is gone: it now types itself in on every page load, and still runs only once per load.

Each section heading now types itself in as it comes into view. A heading already on screen at mount starts straight away; the rest wait for an IntersectionObserver, triggered once its top rises past 85% of the viewport.

Both share the pre-paint arming script, so the server-rendered text never flashes before an animation starts. The static copy stays in the flow but invisible while armed, reserving the height a heading will end up at so a two-line heading does not shove the page down as it types.

Arming now carries a failsafe: if the bundle has not run within four seconds the class is dropped and the static text is shown, so a heading can never be left blank by a script that fails to load.

Also applies the cap-height trim to the case-file heading, which had been rendering untrimmed while every other heading was trimmed.
2026-08-27 23:37:50 +09:00
jinhojang6 4ab68c20cb feat(web): add "Check everyone to catch no one" landing page
Implements the campaign landing page at /catch-no-one from Figma: a hero with a one-shot typewriter headline, five content bands, the search-demand bar chart, two CTA cards, and the source list.

Copy lives in a colocated _content.ts, transcribed verbatim from the design. Styling is Tailwind over the logos-tokens design system, falling back to Figma's exact values only where the token ramp has no match: the #2f2f2f CTA cards, the neutral chart greys, and the #fafaf7 dark-panel ink.

Text primitives carry text-box-trim so the 18/60/112px rhythm is measured cap-to-cap the way Figma measures it, rather than line-box to line-box.

The headline types itself in once per browser session, is skipped under prefers-reduced-motion, and is armed by a pre-paint script so the server-rendered text never flashes before the animation starts.

Also adds Fira Code 700, which the exhibit labels need and which the browser was previously synthesising.
2026-08-27 23:37:50 +09:00
Jules 8a70822731 refactor(funnel): single source of truth for funnel form options
* refactor(web): dedupe funnel form options across the 3 forms

`skills`, `chatService` and `country` were byte-identical in all three
form definitions. They move to `packages/funnel/src/form-options.ts` and
reach the pages through `AFFORM_OPTIONS`; the form files now describe
rendering only.

Lists verified ordered-identical to the originals, entry for entry.

Closes #140

* refactor(civi-crm): derive notion id/label maps from shared funnel options

`SKILLS_MAP`, `CHAT_SERVICE_MAP` and `COUNTRY_MAP` were a fourth copy of
the funnel option lists. They are now `toLabelMap()` over the shared
arrays, the way `HEAR_ABOUT_MAP` already was, so an id cannot mean one
thing in the dropdown and another in Notion.

All 275 derived entries verified identical to the previous literals, so
the Notion and n8n writes are unchanged. The one exception is country
1054, normalised to `Côte d'Ivoire` -- the spelling Notion already stored
and the one the other apostrophised country names use. The dropdown label
and the Ghost member note now match it.
2026-08-26 00:35:44 +09:00
Jules dfe8cf0451 fix(civi-crm): validate required funnel fields on afform-submit
* feat(funnel): add per-form required field list

Single source for which funnel answers are required, plus the check the
intake endpoint runs a submission against.

* refactor(web): build the funnel form schema from the shared list

Form definitions no longer carry a `required` flag, they describe
rendering only. The three pages pass REQUIRED_FIELDS_BY_FORM into
ConnectFormSection, which derives the zod schema and the required markers
from it.

* fix(civi-crm): validate required funnel fields on afform-submit

Only formName, the hear-about answer and the captcha were checked, so a
direct POST could create a record with no name, email or country. A
payload missing a required answer now gets a 400 naming the fields.

* docs(funnel): document the required-field validation
2026-08-24 23:24:13 +09:00
jinhojang6 7af2285712 fix(web): fail the build when the RFP source is unreachable
The sitemap fetched RFPs strictly while generateStaticParams and the RFP
listing used a lenient wrapper that degraded to an empty array. Under
`output: 'export'` there is no runtime fallback, so a GitHub hiccup during
generateStaticParams shipped a site whose RFP detail routes all 404 while
the sitemap still advertised them.

Every caller now uses the throwing fetch, so the page set and the sitemap
always agree. Transient failures (403/429/5xx and network errors) retry
with exponential backoff — including the directory listing, which was
single-shot — while a 404 falls straight through to the next fallback URL
instead of burning two pointless retries.
2026-08-20 23:10:38 +09:00
jinhojang6 e65454efc1 fix(web): harden sitemap RFP sourcing and breadcrumb names
Sitemap RFP entries now come from fetchGithubRfpsStrict, which throws when the Github listing or any individual RFP file cannot be read. Previously a failed fetch silently returned an empty list, so a Github outage shipped a sitemap with no RFP URLs while CI stayed green. The RFP pages keep the lenient loader and still degrade to an empty listing.

The static export smoke test derives RFP routes from the exported HTML instead of making a second Github call, which removes the build-time vs check-time race and makes the check fully offline.

Technology stack pages read the parent /technology-stack page copy for the parent breadcrumb name, instead of deriving it from the child page's own hero eyebrow.

Organization @id and url use the normalised site origin, and the structured data tests derive their base URL from siteConfig rather than hardcoding logos.co.
2026-08-20 23:10:38 +09:00
jinhojang6 f7186c294a fix(web): remove inaccurate sitemap lastmod 2026-08-20 23:10:38 +09:00
jinhojang6 5fcb7c5fa5 feat(web): add structured SEO data 2026-08-20 23:10:38 +09:00
jinhojang6 130a8e029b fix(web): align RFP sitemap with live source 2026-08-20 23:10:38 +09:00
Jules bc30c4cede refactor(civi-crm/web): drop CiviCRM integration
* refactor(civi-crm): remove case management pages and API

The Circle Case UI, its API routes and the CiviCRM query layer they used
are gone. Keeps the app shell out entirely: no pages remain, so the
layout, styles and UI dependencies go with them.

* refactor(civi-crm): drop CiviCRM write from the intake endpoint

Submissions now go to Notion and, for the steward form, n8n. The legacy
afformCircleContactForm name was CiviCRM-only and is no longer accepted;
the fields[] payload is ignored.

* chore(web): remove CiviCRM afform generator scripts

They fetched form definitions from the CiviCRM API, which is gone. The
generated files under lib/civicrm are hand-maintained from now on.

* refactor(web): trim funnel form definitions and move to lib/funnel-forms

Fields keep only what renders the form: formKey, label, inputType,
required, repeatable. The CiviCRM entity/join/fieldName plumbing and the
fields[] POST payload are gone, along with the unused circle contact
form.

country was marked required but exempt from validation through its
CiviCRM Address join, so it is now enforced like every other required
select.

* docs: rewrite civi-crm and funnel docs for the intake-only app

Drops the CiviCRM API guide and the case management architecture, and
updates the funnel reference to the Notion + n8n flow.
2026-08-13 22:44:23 +09:00
jinhojang6 1360bd70df feat: add roadmap page 2026-08-11 04:00:54 +09:00
jinhojang6 542da23c8c fix: adjust circle CTA spacing 2026-08-11 03:53:32 +09:00
jinhojang6 d92ec0319e fix: update Build the Parallel circle CTAs 2026-08-11 03:53:32 +09:00
jinhojang6 d6b47e7679 fix: address Build the Parallel review feedback 2026-08-11 03:53:32 +09:00
jinhojang6 f9140ffc54 fix: centre the hero copy in the visible viewport on phones
The hero is a fixed 760px, which is taller than most phone viewports, so
copy centred within the section still sat low on screen — on a 690px
viewport there was 114px above the block and only 36px below it.

Below `lg`, centre the copy in min(100svh, 760px) instead, floored at
560px so a landscape phone cannot clip it. From `lg` up nothing changes:
the same absolute offsets as the homepage, which keeps rendering
byte-identical markup because the new layout is opt-in per page.
2026-08-10 20:38:46 +09:00
jinhojang6 c6b566b9d9 fix: name the homepage path-card CTAs for Umami
Same problem as /build-the-parallel: the whole card is the link, so the
Umami click tracker was naming the event after the card's entire text —
heading, CTA label and body run together — which made the three cards
indistinguishable in the dashboard.

Attributes only; the rendered markup is otherwise unchanged and the page
is pixel-identical to develop.
2026-08-10 20:38:46 +09:00
jinhojang6 3b3fa853ba feat: name every CTA on /build-the-parallel for Umami
Page views, bounce rate and time on site already come from the site-wide
Umami script, and UmamiButtonTracker already fires on every link click —
but it names the event after the element's text. That breaks on a copy
edit, and for the path cards the whole card is the link, so the event name
was the heading, CTA label and body run together.

Give the hero and path-card sections an optional event-name prop and set a
stable name for all seven CTAs. Both props are optional, so the homepage
keeps rendering byte-identical markup.
2026-08-10 20:38:46 +09:00
jinhojang6 ede041ccc7 revert: drop the centred hero layout, keep the homepage offsets
The centred flex layout spaced the three hero blocks evenly, but the
homepage's offsets are not even (52px above the headline, 78px below), so
the headline landed 22px lower than on the homepage.

Go back to the shared offsets layout on /build-the-parallel. Both pages now
render the hero from the same component with the same absolute positions,
and both overlay their header, so the section starts at page y=0 on each.
2026-08-10 20:38:46 +09:00
jinhojang6 dda4d5c169 fix: centre the hero content block vertically
The hero placed its three copy blocks at hand-tuned absolute offsets, so
the group drifted a little below the section's centre and the drift moved
with the viewport and the length the copy wrapped to.

Add a `centred` content layout that stacks the same blocks in a flex
column with an even 70px rhythm, centred in the hero, and use it on
/build-the-parallel. The homepage keeps the existing offsets layout and
renders byte-identical markup.
2026-08-10 20:38:46 +09:00
jinhojang6 d33b122ccd fix: overlay the header and match the Figma footer spacing
The header reserved its 40/42px bar on this page, so a beige strip sat
above the hero. Register /build-the-parallel with the existing hero-tone
header list so the nav floats over the hero image the way Figma draws it.

Figma leaves 224px between the path cards and the footer; the shared
choose-your-path section only contributes 56px, so the page adds the
rest and scales it down below `lg`.
2026-08-10 20:38:46 +09:00
jinhojang6 f4e876f351 feat: add the /build-the-parallel landing page
Compose the Figma landing page entirely from existing sections: the
shared hero (still image + its own copy), the statement heading with two
CTAs, the Movement page's centred lead-in above the Circles map, and the
choose-your-path cards.

Copy lives in content/pages/en/build-the-parallel.json using existing
section schemas, so no schema changes were needed. British spelling
follows the rest of the site rather than the mixed spelling in Figma.
2026-08-10 20:38:46 +09:00
jinhojang6 2771e99d32 refactor: promote reusable home sections into sections/shared
Move the hero and choose-your-path card sections out of sections/home so
pages other than the homepage can compose them, extract the two-line
statement heading, and lift CenterCtaSection out of the Movement page's
private atoms module.

The hero now takes an optional `background` node, defaulting to the same
Logos background video the homepage has always rendered.

Rendered output is unchanged: the homepage's hero, social proof, and
choose-your-path markup and the Movement page body are byte-identical to
develop, and the generated stylesheet loses no rules.
2026-08-10 20:38:46 +09:00
Jules cc621dcfb1 feat(web): forward submitted form data on newsletter signup
* chore(web): add a debug level to the logger

Logs in every build but tests, for output a deployed preview needs.

* feat(web): forward submitted form data on newsletter signup

Every field a form collects is sent to the subscribe endpoint, select
values as their labels rather than CiviCRM option ids. Logs the payload.

* docs(funnel): document the forwarded subscribe payload

* fix(web): build the funnel newsletter payload from answers only

Keeps the intake captcha token and field defs out of the newsletter
service whatever a caller puts in extraPayload.
2026-08-05 19:42:15 +09:00
jinhojang6 86b4fccbcd fix: redirect brand kit to design guide 2026-07-31 01:51:19 +09:00
jinhojang6 cae38b5769 fix: load shared fonts on global 404 2026-07-31 01:51:19 +09:00
jinhojang6 03feae5d96 fix: simplify the not found page 2026-07-31 01:51:19 +09:00
jinhojang6 ec8c4a7498 fix: redesign the not found page 2026-07-31 01:51:19 +09:00
jinhojang6 65146bdc95 fix: avoid Basecamp download redirect page 2026-07-31 01:51:19 +09:00
jinhojang6 7410b469c8 fix: resolve Basecamp downloads by platform 2026-07-31 01:51:19 +09:00
jinhojang6 10c52c6d2e fix: add legacy tech stack redirect 2026-07-31 01:51:19 +09:00
Jinho Jang 3e36fc9790 Increase small text to 12px (#114)
* fix: increase small text to 12px

* fix: use initiative CTA label

* refactor: use Tailwind text-xs utility

* fix: make homepage banner dismissible

* fix: update Discord links and banner text

* fix: refresh Discord invite links
2026-07-23 20:58:59 +09:00
jinhojang6 dc6b664fc6 fix: align λ lockup to brand guide 2026-07-23 00:28:12 +09:00
jinhojang6 2f29987dd8 fix: update umami data-domains to remove unnecessary entries 2026-07-16 22:25:26 +09:00
jinhojang6 9b2aa2023f chore: enable umami on PR preview 2026-07-16 22:24:15 +09:00