Remove @apply — Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Replace all @apply directives with inline Tailwind utility classes on elements, delete separate CSS files, and keep only minimal <style> blocks for non-inlineable CSS.

Architecture: Component-by-component migration from simplest to most complex. Each task is one component. Inline Tailwind classes replace @apply rules. Remaining CSS (pseudo-elements, vendor prefixes, custom cursors) stays in scoped <style> blocks. The @theme block and Tailwind imports remain in BaseLayout.css.

Tech Stack: Astro, Tailwind CSS v4.2, TypeScript

Design doc: docs/plans/2026-02-27-remove-apply-design.md


Verification approach

There are no visual regression tests. After each task:

  1. Run just app::build to verify no compilation errors
  2. Visually confirm via dev server if desired (just app::serve)

Task 1: SpotifyPlayer

Files:

  • Modify: app/src/components/SpotifyPlayer/SpotifyPlayer.astro

Step 1: Replace class with inline Tailwind + style attribute

The .spotify-player class maps to @apply mt-auto pt-3 plus three non-inlineable properties. Move the Tailwind utilities inline and use a style attribute for the rest.

Replace the entire file content with:

---
import { SPOTIFY_PLAYLIST_ID } from "@lib/site-config";
---
<div class="mt-auto pt-3" style="transform: scale(0.88); transform-origin: bottom left; margin-right: -12%;">
<iframe
data-testid="embed-iframe"
title="Spotify playlist"
src={`https://open.spotify.com/embed/playlist/${SPOTIFY_PLAYLIST_ID}?utm_source=generator&theme=0`}
width="100%"
height="152"
frameBorder="0"
allowfullscreen=""
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
style="border-radius:12px;"></iframe>
</div>

The <style> block and @reference import are removed entirely.

Step 2: Build to verify

Run: just app::build Expected: Build succeeds with no errors

Step 3: Commit

Terminal window
git add app/src/components/SpotifyPlayer/SpotifyPlayer.astro
git commit -m "refactor(SpotifyPlayer): replace @apply with inline Tailwind classes"

Task 2: Chat

Files:

  • Modify: app/src/components/Chat/Chat.astro
  • Delete: app/src/components/Chat/Chat.css

Step 1: Inline all classes onto Chat.astro elements

Replace each element’s class/id with the corresponding Tailwind utilities from Chat.css. Key mappings:

ElementCurrentNew classes
#chat-panel (aside)hidden lg:flex lg:flex-col lg:border-l lg:border-border lg:p-4 font-body gap-1 overflow-y-auto overflow-x-hidden lg:shrink-0 lg:grow-0 lg:w-80 lg:min-w-80
#chat-headerflex flex-col gap-0.5 m-0
#chat-header-topflex items-center gap-1.5
h2 inside headerm-0 font-heading text-base font-semibold text-teal tracking-tight
#chat-status-dotinline-block shrink-0 w-2 h-2 rounded-full bg-muted data-[online=true]:bg-neon
#chat-owner-statusflex-1 text-xs text-muted data-[online=true]:text-neon
#chat-user-counttext-xs text-muted
#chat-overlay-closehidden
#chat-username-barflex items-center gap-1.5
label inside username-barwhitespace-nowrap uppercase tracking-widest font-body text-2xs text-muted
#chat-usernameflex-1 py-0.5 px-1 text-xs font-body bg-transparent text-text min-w-0 focus:outline-none
#chat-roomflex flex-col gap-2 flex-1 min-h-0
#chat-messagesflex-1 text-sm text-text overflow-y-auto min-h-0
#chat-input-barflex items-center gap-1
#chat-inputflex-1 py-2 px-3 border border-border rounded-md text-sm font-body bg-midnight text-text min-w-0 focus:outline-none focus:border-teal
#chat-sendp-2 border-none bg-transparent text-muted text-sm hover:text-neon transition-colors

Note: The #chat-panel element gets overflow + sizing classes merged from both Chat.css and BaseLayout.css (since we’ll remove the BaseLayout #chat-panel rule in Task 9).

Step 2: Add minimal scoped <style> for non-inlineable CSS

Replace the <style is:global> block with a scoped <style> block for the dashed border on #chat-username:

<style>
#chat-username {
border: none;
border-bottom: 1px dashed var(--color-border);
border-radius: 0;
box-shadow: none;
}
#chat-username:focus {
border-bottom-color: var(--color-teal);
}
</style>

Step 3: Delete Chat.css

Delete app/src/components/Chat/Chat.css.

Step 4: Build to verify

Run: just app::build Expected: Build succeeds with no errors

Step 5: Commit

Terminal window
git add app/src/components/Chat/Chat.astro
git rm app/src/components/Chat/Chat.css
git commit -m "refactor(Chat): replace @apply with inline Tailwind, delete Chat.css"

Task 3: ProjectTree

Files:

  • Modify: app/src/components/ProjectTree/ProjectTree.astro
  • Delete: app/src/components/ProjectTree/ProjectTree.css

Step 1: Inline all classes onto ProjectTree.astro elements

Key mappings from ProjectTree.css:

ElementNew classes
nav#site-navfont-body relative xl:flex xl:flex-col min-h-full overflow-y-auto overflow-x-hidden hidden xl:block xl:shrink-0 xl:grow-0 xl:basis-1/5 xl:max-w-64 xl:py-3 xl:pl-3 xl:pr-0
h2hidden xl:block m-0 mb-3 font-display text-xl font-bold text-neon
.nav-heading-link (a inside h2)no-underline text-neon
.nav-search-trigger buttonhidden xl:flex items-center gap-2 w-full py-2 px-3 mb-3 border border-border rounded-lg bg-surface text-muted text-sm font-body transition-colors hover:border-muted/50
span inside search triggerflex-1 text-left
.nav-search-shortcut kbdtext-xs py-0.5 px-2 border border-border rounded bg-surface-active text-muted font-body
.nav-container divbg-surface border border-border rounded-lg p-3
.nav-list ullist-none p-0 m-0 flex flex-col gap-0.5
.nav-dot spanshrink-0 w-1.25 h-1.25 rounded-full bg-current
.nav-row aflex items-center gap-2 w-full no-underline text-muted font-body font-medium transition-colors text-sm tracking-wider py-2 px-3 rounded-md hover:bg-surface-active hover:text-text data-[active]:bg-surface-active data-[active]:text-teal
.nav-row__label spanflex-1
.nav-row__count spantext-xs font-normal bg-midnight py-0.5 px-2 rounded-full text-muted

Step 2: Handle the data-active count color change

The rule .nav-row[data-active] .nav-row__count changes the count badge style when the parent row is active. Use Tailwind’s group pattern:

  • Add group to each .nav-row anchor element
  • On .nav-row__count span, add: group-data-[active]:bg-teal/15 group-data-[active]:text-teal

Step 3: Handle #nav-panel #site-nav context

The #nav-panel #site-nav rule sets flex flex-col block p-3 when ProjectTree is inside the nav panel. Since ProjectTree is rendered in two places (sidebar and panel), keep a minimal <style> block:

<style>
:global(#nav-panel) #site-nav {
display: flex;
flex-direction: column;
padding: 0.75rem;
}
</style>

Step 4: Delete ProjectTree.css

Delete app/src/components/ProjectTree/ProjectTree.css.

Step 5: Build to verify

Run: just app::build Expected: Build succeeds with no errors

Step 6: Commit

Terminal window
git add app/src/components/ProjectTree/ProjectTree.astro
git rm app/src/components/ProjectTree/ProjectTree.css
git commit -m "refactor(ProjectTree): replace @apply with inline Tailwind, delete ProjectTree.css"

Task 4: CommandPalette

Files:

  • Modify: app/src/components/CommandPalette/CommandPalette.astro
  • Modify: app/src/components/CommandPalette/CommandPalette.ts
  • Delete: app/src/components/CommandPalette/CommandPalette.css

Step 1: Inline classes on CommandPalette.astro template elements

ElementCurrent classNew classes
#command-palette divcp-overlayhidden fixed top-0 left-0 w-full h-full z-1000
.cp-backdrop divcp-backdropabsolute inset-0 bg-midnight/70 backdrop-blur-sm
.cp-dialog divcp-dialogrelative w-full max-w-full mt-12 rounded-none md:max-w-lg md:mt-0 md:rounded-lg bg-surface overflow-hidden font-body flex flex-col max-h-1/2 md:max-h-3/5 shadow-dialog
.cp-header divcp-headerflex items-center gap-2 py-3 px-4 border-b border-border
#cp-collection-selectcp-selectpy-2 px-3 border border-border rounded-md text-xs font-body bg-midnight text-text outline-none min-w-24 hover:border-muted/50 focus:border-teal focus:ring-2 focus:ring-teal/20
#cp-inputcp-inputflex-1 border-none outline-none text-sm py-0.5 font-body bg-transparent text-text
.cp-results divcp-resultsoverflow-y-auto flex-1 p-2
.cp-footer divcp-footerhidden md:flex gap-4 py-2 px-4 border-t border-border text-xs text-muted
span inside footerflex items-center gap-1
kbd inside footertext-xs py-0.5 px-2 border border-border rounded-sm bg-midnight font-body text-muted

Step 2: Update JS-generated HTML in CommandPalette.ts

The renderResults function creates DOM via innerHTML strings. Update the class attributes in the template literals:

Old classNew inline classes
cp-emptypy-8 px-4 text-center text-muted text-sm
cp-row__tagsflex flex-wrap gap-1
cp-row__tagpy-0.5 px-2 bg-transparent border border-teal text-xs text-teal capitalize rounded
cp-rowcp-row flex items-center justify-between gap-3 py-2 px-3 rounded-md no-underline text-muted transition-colors hover:bg-midnight hover:text-text
cp-row__imgw-8 h-8 rounded object-cover shrink-0 bg-midnight
cp-row__img cp-row__img--emptyw-8 h-8 rounded object-cover shrink-0 bg-midnight flex items-center justify-center text-xs font-semibold uppercase font-display border border-border
cp-row__img--empty span (inner)gradient-text (keep using the global utility from BaseLayout.css)
cp-row__contentflex-1 min-w-0 flex flex-col gap-1
cp-row__titlecp-row__title text-sm font-heading font-medium truncate text-text
cp-row__metatext-xs text-muted shrink-0
cp-group-labelcp-group-label py-3 px-2 pb-2 text-xs font-semibold uppercase tracking-wider text-muted first:pt-1

Important: Keep cp-row as a class on the row elements because the JS uses querySelectorAll(".cp-row") for keyboard navigation and selection toggling. Keep cp-row__title too since the selected state targets it.

Step 3: Handle open/close toggle classes in JS

The JS adds/removes cp-overlay--open. Change this to toggle the specific classes instead:

In the open() function, instead of overlay.classList.add("cp-overlay--open"), add individual classes:

overlay.classList.add("flex", "items-start", "md:items-center", "justify-center");
overlay.classList.remove("hidden");

In the close() function, instead of overlay.classList.remove("cp-overlay--open"):

overlay.classList.remove("flex", "items-start", "md:items-center", "justify-center");
overlay.classList.add("hidden");

Update isOpen() to check !overlay.classList.contains("hidden").

Step 4: Handle selected state in JS

The updateSelection function toggles cp-row--selected. Change to toggle individual Tailwind classes:

function updateSelection() {
const items = resultsContainer.querySelectorAll(".cp-row");
items.forEach((el, i) => {
const isSelected = i === selectedIndex;
el.classList.toggle("bg-surface-active", isSelected);
el.classList.toggle("text-teal", isSelected);
// Update title color
const title = el.querySelector(".cp-row__title");
if (title) title.classList.toggle("text-teal", isSelected);
// Update tag styles
el.querySelectorAll(".cp-row__tag").forEach((tag) => {
tag.classList.toggle("bg-teal/10", isSelected);
});
if (isSelected) el.scrollIntoView({ block: "nearest" });
});
}

Note: bg-teal/10 as a class toggle may not work with Tailwind JIT since it uses / syntax. Instead, handle the selected row tag styles differently — either use a data attribute approach or keep a small CSS rule. The cleanest approach: add data-selected to the row, and use a tiny <style> block for tag selected state. Or, simply keep the .cp-row--selected .cp-row__tag in CSS.

Revised approach for selection: Keep cp-row--selected as a toggle class, and add a small <style> block:

<style>
.cp-input::placeholder {
color: color-mix(in srgb, var(--color-muted) 60%, transparent);
}
.cp-group-label:first-child {
padding-top: 0.25rem;
}
.cp-row--selected {
background-color: var(--color-surface-active);
color: var(--color-teal);
}
.cp-row--selected .cp-row__title {
color: var(--color-teal);
}
.cp-row--selected .cp-row__tag {
background-color: color-mix(in srgb, var(--color-teal) 10%, transparent);
border-color: var(--color-teal);
color: var(--color-teal);
}
.cp-row__img--empty span {
background: linear-gradient(135deg, var(--color-neon) 0%, var(--color-teal) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
</style>

Keep cp-row--selected, cp-row__title, cp-row__tag, and cp-row__img--empty as class names in the JS-generated HTML since the CSS targets them.

Step 5: Remove CSS import from .astro file, delete CommandPalette.css

Remove import "./CommandPalette.css" from the frontmatter. Delete app/src/components/CommandPalette/CommandPalette.css.

Step 6: Build to verify

Run: just app::build Expected: Build succeeds with no errors

Step 7: Commit

Terminal window
git add app/src/components/CommandPalette/CommandPalette.astro app/src/components/CommandPalette/CommandPalette.ts
git rm app/src/components/CommandPalette/CommandPalette.css
git commit -m "refactor(CommandPalette): replace @apply with inline Tailwind, delete CSS file"

Task 5: about.astro

Files:

  • Modify: app/src/pages/about.astro

Step 1: Inline classes onto elements

ElementCurrent classNew classes
articleabout-pagefont-body max-w-3xl mx-auto
h2m-0 mb-2 font-display font-bold text-neon text-display-sm tracking-tighter leading-none
Each h3font-heading text-xl font-semibold text-teal mt-6 mb-2 tracking-tight
Each ptext-text mb-4 leading-relaxed
Each ultext-text mb-4 pl-4 list-disc
Each limb-2 leading-relaxed
.about-photos divabout-photosgrid grid-cols-2 sm:grid-cols-4 gap-2 mb-6
Each Imageabout-photow-full h-auto border border-border rounded-lg

Step 2: Handle link hover styles

The rule .about-page a { text-teal hover:text-neon } targets all links inside the article. Since there are multiple <a> tags in the markup, add text-teal hover:text-neon to each link element. The global a { text-teal } from BaseLayout.css already covers the base color, so we only need to ensure hover:text-neon is present (which is also global). We can simply remove this rule — the global styles already handle it.

Step 3: Remove the entire <style> block

The <style> block (including @reference) is removed entirely since all rules are now inline.

Step 4: Build to verify

Run: just app::build Expected: Build succeeds with no errors

Step 5: Commit

Terminal window
git add app/src/pages/about.astro
git commit -m "refactor(about): replace @apply with inline Tailwind, remove style block"

Task 6: [collection]/[…id].astro (Post page)

Files:

  • Modify: app/src/pages/[collection]/[...id].astro

Step 1: Inline classes onto template elements

ElementCurrent classNew classes
articlepost-pagefont-body max-w-3xl mx-auto
.post-breadcrumb ppost-breadcrumbm-0 mb-3 font-body uppercase text-neon font-semibold text-2xs tracking-widest
a inside breadcrumbtext-neon no-underline hover:text-teal
.post-header headerpost-headermb-6
h1 inside headerm-0 mb-3 font-display font-bold text-neon text-display tracking-tighter leading-display
.post-description ppost-descriptionm-0 mb-3 font-heading font-semibold text-teal text-subtitle tracking-tight
.post-meta divpost-metaflex flex-wrap items-center gap-3
.post-date timepost-datetext-sm text-muted
.post-content divpost-content prose prose-invert prose-sm max-w-none text-text font-body

Step 2: Keep :global() rules in scoped <style>

The :global() rules for markdown-rendered content must stay. Replace the <style> block with only the :global() rules (no @reference needed since we’re using plain CSS properties, not @apply):

<style>
.post-content :global(h2),
.post-content :global(h3),
.post-content :global(h4) {
font-family: var(--font-heading);
color: var(--color-text);
}
.post-content :global(h2) {
color: var(--color-teal);
}
.post-content :global(.expressive-code) {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
}
.post-content :global(code:not(pre code)) {
font-family: var(--font-body);
background-color: var(--color-surface);
border: 1px solid var(--color-border);
color: var(--color-teal);
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.75rem;
}
.post-content :global(a) {
color: var(--color-teal);
}
.post-content :global(a):hover {
color: var(--color-neon);
}
.post-content :global(img) {
border-radius: 0.5rem;
}
</style>

Note: We can’t use @apply inside :global() rules without @reference, and we’re removing @reference. So we use plain CSS property values referencing the theme vars. Alternatively, we could keep @reference and @apply only for the :global() rules since those truly can’t be inlined. The simpler approach: keep @reference and @apply only for the :global() rules.

Revised <style> block:

<style>
@reference "../../layouts/BaseLayout/BaseLayout.css";
.post-content :global(h2),
.post-content :global(h3),
.post-content :global(h4) {
@apply font-heading text-text;
}
.post-content :global(h2) {
@apply text-teal;
}
.post-content :global(.expressive-code) {
@apply my-6;
}
.post-content :global(code:not(pre code)) {
@apply font-body bg-surface border border-border text-teal px-1.5 py-0.5 rounded text-xs;
}
.post-content :global(a) {
@apply text-teal hover:text-neon;
}
.post-content :global(img) {
@apply rounded-lg;
}
</style>

This is the one place where @apply with @reference is justified — these :global() selectors cannot be inlined.

Step 3: Build to verify

Run: just app::build Expected: Build succeeds with no errors

Step 4: Commit

Terminal window
git add "app/src/pages/[collection]/[...id].astro"
git commit -m "refactor(post-page): replace @apply with inline Tailwind where possible, keep :global() rules"

Task 7: index.astro (Home page)

Files:

  • Modify: app/src/pages/index.astro

Step 1: Inline classes onto template elements

ElementCurrent classNew classes
.home-hero sectionhome-heroflex flex-col items-center text-center gap-8
.home-hero__text divhome-hero__textmax-w-lg
.home-hero__title h1home-hero__titlem-0 font-display font-bold text-neon text-display-lg tracking-tighter leading-none
.home-hero__body phome-hero__bodym-0 mt-4 font-heading text-muted leading-relaxed text-subtitle tracking-tight
.home-section section (×2)home-sectionmt-16
.home-section__header div (×2)home-section__headerflex items-center gap-3 mb-6
.home-section__label span (×2)home-section__labelfont-body text-2xs text-muted uppercase tracking-widest whitespace-nowrap
.home-section__rule hr (×2)home-section__rulegrow border-t border-border my-0
.home-section__link a (×2)home-section__linkfont-body text-xs text-teal no-underline whitespace-nowrap tracking-wider inline-flex items-center gap-1 hover:text-neon
.home-section__arrow svg (×2)home-section__arrowinline-block (plus style="width:0.55em;height:0.55em")
.home-projects divhome-projectsgrid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3
.home-project-card a (×N)home-project-cardgroup block no-underline bg-surface border border-border rounded-lg overflow-hidden transition-all duration-200 hover:-translate-y-1 hover:shadow-card-hover
.home-project-card__image imghome-project-card__imagew-full object-cover (plus style="aspect-ratio:16/10")
.home-project-card__body divhome-project-card__bodyp-4
.home-project-card__category spanhome-project-card__categoryfont-body text-2xs text-muted uppercase tracking-widest
.home-project-card__title h3home-project-card__titlem-0 mt-1 font-heading text-text text-sm font-medium
.home-project-card__desc phome-project-card__descm-0 mt-2 font-body text-muted text-xs leading-relaxed line-clamp-2

Note: Tailwind v4 has line-clamp-2 which handles the webkit line clamp prefixes. The hover:shadow-card-hover uses the custom shadow token defined in @theme.

For the timeline section:

ElementNew classes
.home-timeline divhome-timeline relative overflow-x-auto
.home-timeline__track divhome-timeline__track flex gap-6 pb-4 relative (plus style="min-width:max-content")
.home-timeline__item agroup home-timeline__item flex flex-col items-center no-underline relative (plus style="flex:0 0 auto;width:90px")
.home-timeline__thumb divw-20 h-14 rounded overflow-hidden bg-surface border border-border relative z-10 group-hover:border-teal
img inside thumbw-full h-full object-cover
.home-timeline__thumb-placeholder divw-full h-full bg-surface
.home-timeline__year spanfont-body text-2xs text-text mt-2 font-medium group-hover:text-teal
.home-timeline__name spanhome-timeline__name font-body text-muted mt-0.5 text-center leading-tight

For .home-timeline__item--current, use class:list conditional to add [&_.home-timeline__thumb]:border-neon or handle via a scoped style.

Step 2: Keep minimal <style> block for non-inlineable CSS

<style>
@reference "../layouts/BaseLayout/BaseLayout.css";
/* Web component overrides */
live-window {
--window-color: var(--color-midnight);
--blinds-color: var(--color-midnight);
--clock-text-color: #e74c3c;
--weather-text-font: "Space Grotesk", sans-serif;
--weather-text-color: var(--color-teal);
--weather-text-margin: 12px 0;
--weather-text-size: 14px;
--weather-text-transform: uppercase;
--weather-text-letter-spacing: 0.08em;
}
/* Timeline: horizontal scroll fade */
.home-timeline {
-webkit-overflow-scrolling: touch;
mask-image: linear-gradient(to right, transparent, black 2%, black 98%, transparent);
}
/* Timeline: track line pseudo-element */
.home-timeline__track::before {
content: "";
@apply absolute left-0 right-0 border-t border-border;
top: 40px;
}
/* Timeline: current version glow */
.home-timeline__item--current .home-timeline__thumb {
@apply border-neon;
box-shadow: 0 0 8px rgba(57, 255, 20, 0.3);
}
/* Timeline: name font size */
.home-timeline__name {
font-size: 0.6rem;
}
</style>

Note: We keep @reference here because the ::before and .home-timeline__item--current rules use @apply.

Step 3: Build to verify

Run: just app::build Expected: Build succeeds with no errors

Step 4: Commit

Terminal window
git add app/src/pages/index.astro
git commit -m "refactor(home): replace @apply with inline Tailwind, keep minimal style block"

Task 8: CollectionGrid

Files:

  • Modify: app/src/components/CollectionGrid/CollectionGrid.astro
  • Delete: app/src/components/CollectionGrid/CollectionGrid.css

This is the largest component. It has multiple card variants (stacked, gallery, link, flag) with parent-child hover effects.

Step 1: Inline classes on page-level elements

ElementCurrent classNew classes
.collection-page divcollection-pagefont-body max-w-5xl mx-auto
.collection-hero headercollection-heromb-6
h1 inside herom-0 mb-3 font-display font-bold text-neon text-display tracking-tighter leading-display
.collection-subtitle pcollection-subtitlem-0 mb-3 font-heading font-semibold text-teal text-subtitle tracking-tight
.collection-count pcollection-countm-0 font-body text-muted text-sm
.filter-section divfilter-sectionmb-6
.filter-tags divfilter-tagsflex flex-wrap gap-2

Step 2: Inline filter tag classes

For .filter-tag anchors, use class:list:

<a
href={`/${collection}`}
class:list={[
"bg-transparent border border-teal text-teal font-body transition-colors no-underline capitalize text-xs rounded-sm py-1 px-3 tracking-wide hover:bg-teal/10 hover:text-teal",
{ "bg-teal/15 text-teal border-teal": !activeCategory }
]}
>

Step 3: Inline grid classes

The .items-grid div uses class:list already. Update:

<div class:list={[
"items-grid grid gap-4 grid-cols-1 sm:grid-cols-2 2xl:grid-cols-3",
{ "grid-cols-2 gap-4": useGallery }
]}>

Keep items-grid as a class name since the JS masonry code uses querySelector(".items-grid").

Step 4: Inline stacked card classes

Add group to the card for parent-child hover effects:

<a href={href} class="group item-card bg-surface border border-border no-underline text-inherit transition-all rounded-lg hover:-translate-y-0.5 hover:border-muted/40 hover:shadow-card-hover flex flex-col overflow-hidden">

Keep item-card as a class since the masonry JS iterates .item-card children.

Child elements:

ElementNew classes
.item-card__image-wrapper divoverflow-hidden bg-midnight border-b border-border h-24 rounded-t-sm
.item-card__image-wrapper--color variantAdd relative when using color placeholder
.item-card__image imgw-full h-full object-cover transition-transform duration-300 group-hover:scale-105
.item-card__body div (stacked)flex flex-col gap-1 p-3
.item-card__meta pm-0 font-body uppercase text-neon font-semibold text-2xs tracking-widest
.item-card__title h3 (stacked)m-0 font-heading font-bold text-text leading-snug text-base
.item-card__description pm-0 font-body text-muted leading-relaxed text-xs line-clamp-2
.item-tags divflex flex-wrap gap-1 mt-3
.item-tag spanbg-transparent border border-teal text-teal capitalize text-2xs rounded-sm py-0.5 px-2 tracking-wide

Step 5: Inline gallery card classes

<a href={href} class="group item-card bg-surface border border-border no-underline text-inherit transition-all rounded-lg hover:-translate-y-0.5 hover:border-muted/40 hover:shadow-card-hover overflow-hidden flex flex-col max-h-80">
ElementNew classes
.item-card__gallery-image-wrapperoverflow-hidden flex-1
.item-card__gallery-imagew-full h-full object-cover object-center transition-transform duration-500 group-hover:scale-105
.item-card__gallery-placeholderw-full h-full aspect-4/3
.item-card__gallery-bodyp-3 flex flex-col gap-0.5
.item-card__gallery-titlem-0 font-heading font-bold text-text text-sm leading-snug
.item-card__gallery-categoryfont-body uppercase text-neon font-semibold text-2xs tracking-widest

Step 6: Inline link card classes

<a href={href} class="item-card bg-surface border border-border no-underline text-inherit transition-all rounded-lg hover:-translate-y-0.5 hover:border-muted/40 hover:shadow-card-hover flex flex-col gap-1 p-3" target="_blank" rel="noopener">
ElementNew classes (link variant)
.item-card__meta p (link)m-0 text-muted flex items-center gap-1 text-2xs normal-case
.item-card__external-icon svgtext-muted opacity-60
.item-card__title h3 (link)m-0 font-heading font-bold text-text leading-snug flex items-center gap-2 text-base
.item-card__favicon imgshrink-0 inline-block rounded-sm align-middle
.item-card__favicon-placeholder spanshrink-0 font-display font-bold uppercase text-xs w-4 text-center
.item-card__description p (link)m-0 font-body text-muted leading-relaxed text-xs

Step 7: Inline flag card classes

<a href={href} class="group item-card bg-surface border border-border no-underline text-inherit transition-all rounded-lg hover:-translate-y-0.5 hover:border-muted/40 hover:shadow-card-hover flex flex-row items-center p-3 gap-4">
ElementNew classes (flag variant)
.item-card__flag-image divshrink-0 flex items-center justify-center bg-midnight border border-border overflow-hidden w-16 h-16 rounded-sm
.item-card__image img (flag)w-full h-full object-cover
.item-card__placeholder spanfont-display font-bold uppercase text-2xl
.item-card__body div (flag)flex-1 min-w-0 flex flex-col gap-1
.item-card__title h3 (flag)m-0 font-heading font-bold text-text leading-snug text-base
.item-card__description p (flag)m-0 font-body text-muted leading-relaxed text-xs

Step 8: Add minimal <style> for masonry

Replace the @import style block with:

<style>
.items-grid.masonry-ready {
gap: 0 1rem;
grid-auto-rows: 10px;
}
.items-grid.masonry-ready > .item-card {
margin-bottom: 1rem;
}
</style>

Step 9: Delete CollectionGrid.css

Delete app/src/components/CollectionGrid/CollectionGrid.css.

Step 10: Build to verify

Run: just app::build Expected: Build succeeds with no errors

Step 11: Commit

Terminal window
git add app/src/components/CollectionGrid/CollectionGrid.astro
git rm app/src/components/CollectionGrid/CollectionGrid.css
git commit -m "refactor(CollectionGrid): replace @apply with inline Tailwind, delete CSS file"

Task 9: BaseLayout

Files:

  • Modify: app/src/layouts/BaseLayout/BaseLayout.css
  • Modify: app/src/layouts/BaseLayout/BaseLayout.astro

Step 1: Inline layout element classes onto BaseLayout.astro

ElementCurrent ID/classNew classes to add
#toolbar divflex items-center justify-between fixed top-0 left-0 right-0 lg:right-80 h-12 px-3 bg-surface border-b border-border z-60 xl:hidden
#toolbar-title afont-display font-bold text-base text-neon no-underline
#toolbar-nav navflex xl:hiddenflex xl:hidden items-center gap-1 overflow-x-auto
.toolbar-nav-link a (×N)toolbar-nav-linkfont-body text-xs text-muted no-underline whitespace-nowrap py-1 px-2 rounded-md hover:text-text hover:bg-surface-active transition-colors data-[active]:text-teal
#toolbar-menu-btn buttonbg-transparent border-none p-2 text-muted hover:text-neon
#toolbar-search-btn buttonbg-transparent border-none p-1 text-muted hover:text-neon transition-colors
#nav-backdrop divxl:hiddenhidden fixed top-12 left-0 right-0 lg:right-80 h-[calc(100svh-48px)] z-40 bg-midnight/70 backdrop-blur-xs xl:hidden
#chat-overlay-backdrop divlg:hiddenhidden fixed inset-0 z-40 bg-midnight/70 backdrop-blur-xs lg:hidden
#nav-panel divxl:hiddenfixed top-12 left-0 right-0 lg:right-80 max-h-[60vh] overflow-y-auto bg-midnight/95 backdrop-blur-md border-b border-border z-50 -translate-y-[calc(100%+4px)] transition-transform duration-300 ease-in-out xl:hidden
#layout divflex flex-col lg:flex-row w-screen h-svh
#content-viewer divgrow shrink basis-0 min-w-0 pt-16 pb-4 px-4 sm:px-6 sm:pb-6 xl:pt-6 xl:pb-6 overflow-y-auto overflow-x-hidden
#mobile-chat-fab buttonfixed bottom-4 right-4 z-30 w-12 h-12 rounded-full bg-neon text-midnight flex items-center justify-center shadow-lg lg:hidden hover:bg-teal
#chat-overlay divlg:hiddenfixed bottom-0 left-0 w-full h-[85vh] bg-midnight border-t border-border z-50 translate-y-full transition-transform duration-300 ease-in-out lg:hidden flex flex-col px-4 pt-2 pb-4
#chat-overlay-content divflex flex-col flex-1 min-h-0 font-body gap-2

Note: The JS in BaseLayout.ts toggles .visible on backdrops and .open on panels. These need to stay as toggleable classes. Keep them in CSS.

Step 2: Shrink BaseLayout.css

Remove all rules that are now inlined. The file keeps only:

@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
@theme {
/* ... all existing theme tokens, unchanged ... */
}
/* ---- Global resets ---- */
* {
box-sizing: border-box;
}
body {
margin: 0;
background-color: var(--color-midnight);
color: var(--color-text);
font-family: var(--font-body);
font-size: 0.875rem;
line-height: 1.625;
}
a {
color: var(--color-teal);
}
a:hover {
color: var(--color-neon);
}
hr {
border: none;
border-top: 1px solid var(--color-border);
margin: 1.5rem 0;
}
::selection {
background-color: var(--color-neon);
color: var(--color-midnight);
}
/* ---- Custom cursors ---- */
/* ... all cursor rules, unchanged ... */
/* ---- Gradient text utility ---- */
.gradient-text {
background: linear-gradient(135deg, var(--color-neon) 0%, var(--color-teal) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ---- JS-toggled states ---- */
#nav-backdrop.visible,
#chat-overlay-backdrop.visible {
display: block;
}
#nav-panel.open {
transform: translateY(0);
}
#chat-overlay.open {
transform: translateY(0);
}
#chat-overlay #chat-overlay-close {
display: block;
background: transparent;
border: none;
padding: 0.25rem;
color: var(--color-muted);
}
#chat-overlay #chat-overlay-close:hover {
color: var(--color-neon);
}
/* ---- Content viewer media ---- */
#content-viewer img,
#content-viewer video {
max-width: 100%;
height: auto;
}

Note: The global resets (*, body, a, hr, ::selection) are converted from @apply to plain CSS since they’re global rules that should stay in the stylesheet. The cursor rules stay exactly as-is. The .gradient-text utility stays. The JS-toggled states (.visible, .open) stay because they’re toggled by BaseLayout.ts.

Step 3: Build to verify

Run: just app::build Expected: Build succeeds with no errors

Step 4: Commit

Terminal window
git add app/src/layouts/BaseLayout/BaseLayout.astro app/src/layouts/BaseLayout/BaseLayout.css
git commit -m "refactor(BaseLayout): inline Tailwind classes on layout elements, shrink CSS to globals-only"

Task 10: Final cleanup and verification

Step 1: Full build

Run: just app::build Expected: Clean build with no errors or warnings

Step 2: Grep for leftover @apply usage

Run: grep -r "@apply" app/src/ --include="*.css" --include="*.astro" -l

Expected: Only these files should contain @apply:

  • app/src/layouts/BaseLayout/BaseLayout.css — NO, this file should have plain CSS now
  • app/src/pages/[collection]/[...id].astro — YES, :global() rules
  • app/src/pages/index.astro — YES, ::before and nested descendant rules

Step 3: Grep for leftover @reference usage

Run: grep -r "@reference" app/src/ --include="*.css" --include="*.astro" -l

Expected: Only these files:

  • app/src/pages/[collection]/[...id].astro
  • app/src/pages/index.astro

Step 4: Verify deleted CSS files are gone

Confirm these no longer exist:

  • app/src/components/Chat/Chat.css
  • app/src/components/ProjectTree/ProjectTree.css
  • app/src/components/CommandPalette/CommandPalette.css
  • app/src/components/CollectionGrid/CollectionGrid.css

Step 5: Commit any final fixes

Terminal window
git add -A
git commit -m "refactor: final cleanup after @apply removal"