diff --git a/eslint.config.js b/eslint.config.js
index aa8b4ef9..33eae3bb 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -14,7 +14,14 @@ export default defineConfig([
},
tseslint.configs.recommended,
eslintPluginAstro.configs.recommended,
- globalIgnores(['dist', '.astro', 'node_modules', 'public', '**/*.min.js']),
+ globalIgnores([
+ 'dist',
+ '.astro',
+ '.netlify',
+ 'node_modules',
+ 'public',
+ '**/*.min.js'
+ ]),
{
rules: {
'no-console': 'error',
diff --git a/src/components/RoadmapBoard.astro b/src/components/RoadmapBoard.astro
index 5526e7a2..baaa7931 100644
--- a/src/components/RoadmapBoard.astro
+++ b/src/components/RoadmapBoard.astro
@@ -32,6 +32,11 @@ const now = new Date()
const nowYear = now.getUTCFullYear()
const nowMonth = now.getUTCMonth()
const nowLeft = pos.pctLeft(now).toFixed(2)
+const refreshedAt = now.toLocaleDateString('en-US', {
+ month: 'long',
+ day: 'numeric',
+ year: 'numeric'
+})
const gridItems = buildGridItems(projects, teams)
@@ -63,256 +68,278 @@ function fmtDate(iso: string | null | undefined): string {
class="board-frame"
style={`--n-months: ${nMonths}; --n-quarters: ${nQuarters}`}
>
-
-
+
@@ -359,11 +386,12 @@ function fmtDate(iso: string | null | undefined): string {
display: none;
}
- /* Header grid — same column template as .board */
- .board-header-grid {
- display: grid;
- grid-template-columns: 300px repeat(var(--n-months), minmax(88px, 1fr));
- min-width: calc(300px + var(--n-months) * 88px);
+ /* Header table — same column template as .board */
+ .board-header-table {
+ table-layout: fixed;
+ border-collapse: separate;
+ border-spacing: 0;
+ width: max(calc(300px + var(--n-months) * 88px), 100%);
font-family: 'Titillium', Arial, sans-serif;
font-size: var(--step--1);
color: #1a1a1a;
@@ -374,17 +402,26 @@ function fmtDate(iso: string | null | undefined): string {
overflow-x: auto;
}
- /* ── Body grid ── */
+ /* ── Body table ── */
.board {
- display: grid;
- grid-template-columns: 300px repeat(var(--n-months), minmax(88px, 1fr));
- min-width: calc(300px + var(--n-months) * 88px);
- column-gap: 0;
+ table-layout: fixed;
+ border-collapse: separate;
+ border-spacing: 0;
+ width: max(calc(300px + var(--n-months) * 88px), 100%);
font-family: 'Titillium', Arial, sans-serif;
font-size: var(--step--1);
color: #1a1a1a;
}
+ /* Column widths — shared by both the header and body tables */
+ .col-label {
+ width: 300px;
+ }
+
+ .col-month {
+ width: 88px;
+ }
+
/* ── "Project" label — sticky left so it stays visible on horizontal scroll ── */
.hd-label {
position: sticky;
@@ -393,13 +430,12 @@ function fmtDate(iso: string | null | undefined): string {
background: #f9fafb;
border-right: 1px solid #e5e7eb;
padding: 0 0.75rem;
- display: flex;
- align-items: center;
font-weight: 700;
font-size: var(--step--2);
text-transform: uppercase;
letter-spacing: 0.05em;
color: #555;
+ vertical-align: middle;
}
/* ── Quarter header cells (row 1) ── */
@@ -413,9 +449,8 @@ function fmtDate(iso: string | null | undefined): string {
text-transform: uppercase;
letter-spacing: 0.05em;
color: #555;
- display: flex;
- align-items: center;
- justify-content: center;
+ text-align: center;
+ vertical-align: middle;
white-space: nowrap;
}
@@ -433,10 +468,7 @@ function fmtDate(iso: string | null | undefined): string {
font-weight: 500;
color: #777;
text-align: center;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 2px;
+ vertical-align: middle;
white-space: nowrap;
}
@@ -451,7 +483,7 @@ function fmtDate(iso: string | null | undefined): string {
}
.now-badge {
- display: inline-block;
+ display: block;
font-size: 0.55rem;
font-weight: 700;
background: var(--color-primary);
@@ -460,10 +492,14 @@ function fmtDate(iso: string | null | undefined): string {
padding: 0.1em 0.4em;
text-transform: uppercase;
letter-spacing: 0.05em;
+ margin: 2px auto 0;
}
/* ── Team header row ── */
.team-header {
+ position: sticky;
+ left: 0;
+ z-index: 4;
border-top: 1px solid color-mix(in srgb, var(--team-color) 50%, transparent);
border-bottom: 1px solid
color-mix(in srgb, var(--team-color) 50%, transparent);
@@ -471,18 +507,22 @@ function fmtDate(iso: string | null | undefined): string {
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
+ padding: 0;
}
.team-header-label {
- position: sticky;
- left: 0;
- z-index: 4;
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.75rem;
}
+ .team-header-track {
+ border-top: 1px solid color-mix(in srgb, var(--team-color) 50%, transparent);
+ border-bottom: 1px solid
+ color-mix(in srgb, var(--team-color) 50%, transparent);
+ }
+
/* ── Project label cell ── */
.proj-label {
position: sticky;
@@ -491,9 +531,8 @@ function fmtDate(iso: string | null | undefined): string {
/* background-color set via inline style as opaque color-mix() */
border-right: 1px solid var(--team-color);
padding: 0 0.75rem;
- display: flex;
- align-items: center;
- min-height: 3.25rem;
+ vertical-align: middle;
+ height: 3.25rem;
}
.proj-name {
@@ -521,13 +560,9 @@ function fmtDate(iso: string | null | undefined): string {
/* ── Project timeline track ── */
.proj-track {
position: relative;
- background-color: color-mix(
- in srgb,
- rgb(var(--tint-rgb, 255 255 255)) 4%,
- white
- );
- min-height: 3.25rem;
+ height: 3.25rem;
overflow: visible;
+ vertical-align: top;
}
/* "Today" vertical line */
@@ -584,12 +619,15 @@ function fmtDate(iso: string | null | undefined): string {
padding-top: 0.35rem;
z-index: 3;
cursor: default;
+ background: none;
+ border: none;
+ font: inherit;
}
.ms-diamond {
display: block;
- width: 10px;
- height: 10px;
+ width: 20px;
+ height: 20px;
border-radius: 2px;
transform: rotate(45deg);
flex-shrink: 0;
@@ -597,21 +635,64 @@ function fmtDate(iso: string | null | undefined): string {
transition: transform 150ms ease;
}
- .ms-pin:hover .ms-diamond {
+ .ms-pin:hover .ms-diamond,
+ .ms-pin:focus-visible .ms-diamond {
transform: rotate(45deg) scale(1.3);
}
+ .ms-pin:focus-visible {
+ outline: none;
+ }
+
+ .ms-pin:focus-visible .ms-diamond {
+ box-shadow:
+ 0 0 0 2px white,
+ 0 0 0 4px var(--color-primary);
+ }
+
+ .ms-pin:hover,
+ .ms-pin:focus-visible,
+ .ms-pin--active {
+ z-index: 16;
+ }
+
.ms-pin-name {
- font-size: 0.58rem;
+ display: none;
+ }
+
+ /* Tooltip visible on hover, keyboard focus, or click activation */
+ .ms-pin:hover .ms-pin-name,
+ .ms-pin:focus-visible .ms-pin-name,
+ .ms-pin--active .ms-pin-name {
+ display: block;
+ position: absolute;
+ bottom: calc(100% + 8px);
+ left: 50%;
+ transform: translateX(-50%);
+ background: rgba(25, 25, 25, 0.92);
+ color: #fff;
+ padding: 5px 8px;
+ border-radius: 5px;
+ font-size: 0.65rem;
font-weight: 600;
- color: #444;
+ white-space: normal;
+ max-width: 130px;
text-align: center;
- max-width: 72px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- margin-top: 0.25rem;
- line-height: 1.2;
+ line-height: 1.3;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
+ pointer-events: none;
+ }
+
+ .ms-pin:hover .ms-pin-name::after,
+ .ms-pin:focus-visible .ms-pin-name::after,
+ .ms-pin--active .ms-pin-name::after {
+ content: '';
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ border: 5px solid transparent;
+ border-top-color: rgba(25, 25, 25, 0.92);
}
/* ── Scrollbar (body only) ── */
@@ -663,11 +744,19 @@ function fmtDate(iso: string | null | undefined): string {
display: flex;
}
- /* Quarter-column grid for header and body */
- .board-header-grid,
+ /* Quarter-column widths for header and body */
+ .board-header-table,
.board {
- grid-template-columns: 130px repeat(var(--n-quarters), minmax(72px, 1fr));
- min-width: calc(130px + var(--n-quarters) * 72px);
+ width: max(calc(130px + var(--n-quarters) * 72px), 100%);
+ }
+
+ .col-label {
+ width: 130px;
+ }
+
+ .col-month {
+ width: 0;
+ display: none;
}
/* Swap quarter header variants */
@@ -675,10 +764,11 @@ function fmtDate(iso: string | null | undefined): string {
display: none;
}
.hd-quarter--mobile {
- display: flex;
+ display: table-cell;
+ width: 72px;
}
- /* Collapse month header row (keep in DOM so grid positions stay correct) */
+ /* Collapse month header row */
.hd-month {
height: 0;
min-height: 0;
@@ -703,51 +793,23 @@ function fmtDate(iso: string | null | undefined): string {
cursor: pointer;
}
- /* Tooltip popup on tap */
- .ms-pin--active {
- z-index: 10;
- }
-
- .ms-pin--active .ms-pin-name {
- display: block;
- position: absolute;
- bottom: calc(100% + 8px);
- left: 50%;
- transform: translateX(-50%);
- background: rgba(25, 25, 25, 0.92);
- color: #fff;
- padding: 5px 8px;
- border-radius: 5px;
- font-size: 0.65rem;
- font-weight: 600;
- white-space: normal;
- max-width: 130px;
- text-align: center;
- line-height: 1.3;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
- pointer-events: none;
+ /* ── "Full timeline" mode: restore month-column view ── */
+ .board-outer--full .board-header-table,
+ .board-outer--full .board {
+ width: max(calc(300px + var(--n-months) * 88px), 100%);
}
- /* Arrow pointing down */
- .ms-pin--active .ms-pin-name::after {
- content: '';
- position: absolute;
- top: 100%;
- left: 50%;
- transform: translateX(-50%);
- border: 5px solid transparent;
- border-top-color: rgba(25, 25, 25, 0.92);
+ .board-outer--full .col-label {
+ width: 300px;
}
- /* ── "Full timeline" mode: restore month-column view ── */
- .board-outer--full .board-header-grid,
- .board-outer--full .board {
- grid-template-columns: 300px repeat(var(--n-months), minmax(88px, 1fr));
- min-width: calc(300px + var(--n-months) * 88px);
+ .board-outer--full .col-month {
+ width: 88px;
+ display: table-column;
}
.board-outer--full .hd-quarter--desktop {
- display: flex;
+ display: table-cell;
}
.board-outer--full .hd-quarter--mobile {
@@ -766,10 +828,25 @@ function fmtDate(iso: string | null | undefined): string {
.board-outer--full .proj-name-text {
max-width: 240px;
}
+ }
- .board-outer--full .ms-pin-name {
- display: block;
- }
+ .board-caption {
+ caption-side: bottom;
+ text-align: right;
+ font-size: var(--step--2);
+ color: #999;
+ padding: 0.4rem 0.75rem;
+ font-style: italic;
+ }
+
+ .visually-hidden,
+ .sr-only {
+ clip-path: inset(50%);
+ height: 1px;
+ overflow: hidden;
+ position: absolute;
+ white-space: nowrap;
+ width: 1px;
}
@@ -784,9 +861,8 @@ function fmtDate(iso: string | null | undefined): string {
const nowPct = parseFloat(rawPct)
if (isNaN(nowPct)) return
- const labelWidth =
- parseInt(getComputedStyle(board).gridTemplateColumns.split(' ')[0], 10) ||
- 300
+ const labelCell = document.querySelector('.hd-label')
+ const labelWidth = labelCell?.offsetWidth ?? 300
const timelineWidth = board.scrollWidth - labelWidth
const nowPxFromBoardLeft = labelWidth + (nowPct / 100) * timelineWidth