diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs
index 24b79fb117..3934a98326 100644
--- a/services/web/app/src/Features/Project/ProjectController.mjs
+++ b/services/web/app/src/Features/Project/ProjectController.mjs
@@ -1364,6 +1364,7 @@ const _ProjectController = {
function getInitialLoadingScreenTheme(overallThemeSetting) {
switch (overallThemeSetting) {
case 'light-':
+ case 'lumiere-':
return 'light'
case '':
return 'dark'
diff --git a/services/web/app/src/Features/Project/UserSettingsHelper.mjs b/services/web/app/src/Features/Project/UserSettingsHelper.mjs
index e99c74731a..d23ec46d8c 100644
--- a/services/web/app/src/Features/Project/UserSettingsHelper.mjs
+++ b/services/web/app/src/Features/Project/UserSettingsHelper.mjs
@@ -1,4 +1,5 @@
const SYSTEM_THEME_USER_CUTOFF_DATE = new Date(Date.UTC(2026, 2, 2, 12, 0, 0)) // 12pm GMT on March 2, 2026
+const LUMIERE_THEME_USER_CUTOFF_DATE = new Date(Date.UTC(2026, 5, 11, 12, 0, 0)) // 12pm GMT on June 11, 2026
function getOverallTheme(user) {
if (user.ace.overallTheme != null) {
@@ -10,7 +11,11 @@ function getOverallTheme(user) {
return ''
}
- return 'system'
+ if (user.signUpDate < LUMIERE_THEME_USER_CUTOFF_DATE) {
+ return 'system'
+ }
+
+ return 'lumiere-'
}
async function buildUserSettings(_req, _res, user) {
diff --git a/services/web/app/src/infrastructure/ExpressLocals.mjs b/services/web/app/src/infrastructure/ExpressLocals.mjs
index 297880288e..5da9f4b583 100644
--- a/services/web/app/src/infrastructure/ExpressLocals.mjs
+++ b/services/web/app/src/infrastructure/ExpressLocals.mjs
@@ -307,11 +307,15 @@ export default async function (webRouter, privateApiRouter, publicApiRouter) {
webRouter.use(function (req, res, next) {
res.locals.overallThemes = [
{
- name: 'Dark',
+ name: 'Verso Lumière',
+ val: 'lumiere-',
+ },
+ {
+ name: 'Classic Dark',
val: '',
},
{
- name: 'Light',
+ name: 'Classic Light',
val: 'light-',
},
{
diff --git a/services/web/frontend/js/features/project-list/components/project-list-lumiere.tsx b/services/web/frontend/js/features/project-list/components/project-list-lumiere.tsx
new file mode 100644
index 0000000000..950394aedf
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/project-list-lumiere.tsx
@@ -0,0 +1,148 @@
+import { memo } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useProjectListContext } from '../context/project-list-context'
+import { Project } from '../../../../../types/project/dashboard/api'
+import { getOwnerName } from '../util/project'
+import { fromNowDate } from '../../../utils/dates'
+import { ProjectCompiler } from '../../../../../types/project-settings'
+import getMeta from '@/utils/meta'
+import DefaultNavbar from '@/shared/components/navbar/default-navbar'
+import Footer from '@/shared/components/footer/footer'
+import SidebarDsNav from '@/features/project-list/components/sidebar/sidebar-ds-nav'
+import SystemMessages from '@/shared/components/system-messages'
+import CookieBanner from '@/shared/components/cookie-banner'
+import UserNotifications from './notifications/user-notifications'
+import SearchForm from './search-form'
+import NewProjectButton from './new-project-button'
+import ProjectListTitle from './title/project-list-title'
+import LoadMore from './load-more'
+import DashApiError from './dash-api-error'
+
+type FormatVariant = 'latex' | 'typst' | 'quarto' | 'quarto-slides'
+
+function getFormatVariant(
+ compiler: ProjectCompiler | undefined,
+ quartoFlavor: 'revealjs' | 'pdf' | undefined
+): FormatVariant {
+ if (compiler === 'quarto') {
+ return quartoFlavor === 'revealjs' ? 'quarto-slides' : 'quarto'
+ }
+ if (compiler === 'typst') return 'typst'
+ return 'latex'
+}
+
+function getFormatLabel(variant: FormatVariant): string {
+ switch (variant) {
+ case 'typst':
+ return 'Typst'
+ case 'quarto':
+ return 'Quarto'
+ case 'quarto-slides':
+ return 'Quarto Slides'
+ default:
+ return 'LaTeX'
+ }
+}
+
+const ProjectCard = memo(function ProjectCard({
+ project,
+}: {
+ project: Project
+}) {
+ const variant = getFormatVariant(project.compiler, project.quartoFlavor)
+ const ownerName = getOwnerName(project)
+ const date = fromNowDate(project.lastUpdated)
+ const initial = project.name.charAt(0).toUpperCase() || '?'
+
+ return (
+
+
+ {initial}
+
+
+
{project.name}
+
+
+ {getFormatLabel(variant)}
+
+ {ownerName && (
+
+ {ownerName}
+
+ )}
+
+
{date}
+
+
+ )
+})
+
+export function ProjectListLumiere() {
+ const navbarProps = getMeta('ol-navbar')
+ const footerProps = getMeta('ol-footer')
+ const { t } = useTranslation()
+ const {
+ error,
+ visibleProjects,
+ searchText,
+ setSearchText,
+ filter,
+ tags,
+ selectedTagId,
+ } = useProjectListContext()
+
+ const selectedTag = tags.find(tag => tag._id === selectedTagId)
+
+ return (
+
+
+
+
+
+
+
+
+ {error && }
+
+ {visibleProjects.length === 0 ? (
+ {t('no_projects')}
+ ) : (
+
+ {visibleProjects.map(project => (
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+ )
+}
diff --git a/services/web/frontend/js/features/project-list/components/project-list-root.tsx b/services/web/frontend/js/features/project-list/components/project-list-root.tsx
index 352c6fd37a..12bdd1d8a4 100644
--- a/services/web/frontend/js/features/project-list/components/project-list-root.tsx
+++ b/services/web/frontend/js/features/project-list/components/project-list-root.tsx
@@ -17,10 +17,12 @@ import DefaultNavbar from '@/shared/components/navbar/default-navbar'
import Footer from '@/shared/components/footer/footer'
import WelcomePageContent from '@/features/project-list/components/welcome-page-content'
import { ProjectListDsNav } from '@/features/project-list/components/project-list-ds-nav'
+import { ProjectListLumiere } from '@/features/project-list/components/project-list-lumiere'
import { DsNavStyleProvider } from '@/features/project-list/components/use-is-ds-nav'
import CookieBanner from '@/shared/components/cookie-banner'
import useThemedPage from '@/shared/hooks/use-themed-page'
import { UserSettingsProvider } from '@/shared/context/user-settings-context'
+import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { TutorialProvider } from '@/shared/context/tutorial-context'
function ProjectListRoot() {
@@ -80,6 +82,9 @@ function ProjectListPageContent() {
useThemedPage()
const { totalProjectsCount, isLoading, loadProgress } =
useProjectListContext()
+ const {
+ userSettings: { overallTheme },
+ } = useUserSettingsContext()
useEffect(() => {
eventTracking.sendMB('loads_v2_dash', { page: 'projects' })
@@ -105,6 +110,15 @@ function ProjectListPageContent() {
>
)
}
+
+ if (overallTheme === 'lumiere-') {
+ return (
+
+
+
+ )
+ }
+
return (
diff --git a/services/web/frontend/js/features/project-list/components/sidebar/theme-toggle.tsx b/services/web/frontend/js/features/project-list/components/sidebar/theme-toggle.tsx
index 708aed59d7..619d000402 100644
--- a/services/web/frontend/js/features/project-list/components/sidebar/theme-toggle.tsx
+++ b/services/web/frontend/js/features/project-list/components/sidebar/theme-toggle.tsx
@@ -8,6 +8,8 @@ import { useTranslation } from 'react-i18next'
const getIcon = (theme: OverallThemeMeta) => {
switch (theme.val) {
+ case 'lumiere-':
+ return 'auto_awesome'
case 'light-':
return 'light_mode'
case 'system':
diff --git a/services/web/frontend/js/shared/hooks/use-active-overall-theme.tsx b/services/web/frontend/js/shared/hooks/use-active-overall-theme.tsx
index bb95ecfe57..d10dd0bad6 100644
--- a/services/web/frontend/js/shared/hooks/use-active-overall-theme.tsx
+++ b/services/web/frontend/js/shared/hooks/use-active-overall-theme.tsx
@@ -13,7 +13,7 @@ function getTheme(
if (isIEEEBranded()) {
return 'dark'
}
- if (overallTheme === 'light-') {
+ if (overallTheme === 'light-' || overallTheme === 'lumiere-') {
return 'light'
}
if (overallTheme === 'system') {
diff --git a/services/web/frontend/js/shared/utils/styles.ts b/services/web/frontend/js/shared/utils/styles.ts
index c03f3052f7..a897c2877c 100644
--- a/services/web/frontend/js/shared/utils/styles.ts
+++ b/services/web/frontend/js/shared/utils/styles.ts
@@ -1,4 +1,4 @@
-export type OverallTheme = '' | 'light-' | 'system'
+export type OverallTheme = '' | 'light-' | 'system' | 'lumiere-'
export const fontFamilies = {
monaco: ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'monospace'],
diff --git a/services/web/frontend/stylesheets/pages/all.scss b/services/web/frontend/stylesheets/pages/all.scss
index d7570c4ad2..dba082608d 100644
--- a/services/web/frontend/stylesheets/pages/all.scss
+++ b/services/web/frontend/stylesheets/pages/all.scss
@@ -4,6 +4,7 @@
@import 'project-list';
@import 'project-list-default';
@import 'project-list-ds-nav';
+@import 'project-list-lumiere';
@import 'sidebar-v2-dash-pane';
@import 'editor/ide';
@import 'editor/ide-redesign';
diff --git a/services/web/frontend/stylesheets/pages/project-list-lumiere.scss b/services/web/frontend/stylesheets/pages/project-list-lumiere.scss
new file mode 100644
index 0000000000..7a339134cd
--- /dev/null
+++ b/services/web/frontend/stylesheets/pages/project-list-lumiere.scss
@@ -0,0 +1,232 @@
+// Verso Lumière — card-based project dashboard theme
+
+.project-list-lumiere {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ background: #f0f4f8;
+
+ .lumiere-layout {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+
+ // Reuse the existing sidebar but give it a white background in Lumière
+ .project-list-sidebar-wrapper-react {
+ background: #ffffff;
+ border-right: 1px solid #e2e8f0;
+ }
+ }
+
+ .lumiere-main-wrapper {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-width: 0;
+ overflow-y: auto;
+ }
+
+ .lumiere-main {
+ flex: 1;
+ padding: 2rem 2.5rem;
+ max-width: 1400px;
+ width: 100%;
+
+ @media (max-width: 768px) {
+ padding: 1.25rem 1rem;
+ }
+ }
+
+ // ── Header ──────────────────────────────────────────────────────────────
+
+ .lumiere-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-bottom: 2rem;
+ flex-wrap: wrap;
+ }
+
+ .lumiere-header-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ flex-shrink: 0;
+ flex-wrap: wrap;
+ }
+
+ // Override ProjectListTitle so it renders with Lumière typography
+ .lumiere-title {
+ font-family: Georgia, 'Times New Roman', 'DejaVu Serif', serif;
+ font-size: 2.25rem;
+ font-weight: 700;
+ color: #1a2e3b;
+ line-height: 1.15;
+ margin: 0;
+ }
+
+ // ── Empty state ─────────────────────────────────────────────────────────
+
+ .lumiere-empty {
+ color: #64748b;
+ font-size: 0.95rem;
+ margin-top: 2rem;
+ }
+
+ // ── Card grid ───────────────────────────────────────────────────────────
+
+ .lumiere-card-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ gap: 1.5rem;
+ margin-top: 1.5rem;
+ }
+
+ // ── Individual card ──────────────────────────────────────────────────────
+
+ .lumiere-card {
+ display: flex;
+ flex-direction: column;
+ text-decoration: none;
+ border-radius: 10px;
+ background: #ffffff;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
+ transition:
+ transform 0.18s ease,
+ box-shadow 0.18s ease;
+ overflow: hidden;
+ color: inherit;
+
+ &:hover,
+ &:focus-visible {
+ transform: translateY(-3px);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.13);
+ text-decoration: none;
+ color: inherit;
+ }
+ }
+
+ // ── Card thumbnail ───────────────────────────────────────────────────────
+
+ .lumiere-card-thumb {
+ position: relative;
+ height: 130px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+
+ // Folded corner effect
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 0;
+ height: 0;
+ border-style: solid;
+ border-width: 0 28px 28px 0;
+ border-color: transparent rgba(255, 255, 255, 0.3) transparent transparent;
+ z-index: 1;
+ }
+ }
+
+ .lumiere-card-initial {
+ font-family: Georgia, 'Times New Roman', 'DejaVu Serif', serif;
+ font-size: 3rem;
+ font-weight: 700;
+ color: rgba(255, 255, 255, 0.75);
+ line-height: 1;
+ user-select: none;
+ }
+
+ // Format-specific gradients
+ .lumiere-card--latex .lumiere-card-thumb {
+ background: linear-gradient(135deg, #4caf7d 0%, #2a9d8f 100%);
+ }
+
+ .lumiere-card--typst .lumiere-card-thumb {
+ background: linear-gradient(135deg, #2a9d8f 0%, #3d7ebf 100%);
+ }
+
+ .lumiere-card--quarto .lumiere-card-thumb {
+ background: linear-gradient(135deg, #7c4dff 0%, #3d7ebf 100%);
+ }
+
+ .lumiere-card--quarto-slides .lumiere-card-thumb {
+ background: linear-gradient(135deg, #e67e22 0%, #e74c3c 100%);
+ }
+
+ // ── Card body ────────────────────────────────────────────────────────────
+
+ .lumiere-card-body {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ padding: 0.85rem 0.9rem 0.9rem;
+ flex: 1;
+ }
+
+ .lumiere-card-name {
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: #1a2e3b;
+ line-height: 1.3;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
+
+ .lumiere-card-meta {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ flex-wrap: wrap;
+ }
+
+ .lumiere-card-owner {
+ font-size: 0.72rem;
+ color: #64748b;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100px;
+ }
+
+ .lumiere-card-date {
+ font-size: 0.72rem;
+ color: #94a3b8;
+ margin-top: auto;
+ }
+
+ // ── Format badges ────────────────────────────────────────────────────────
+
+ .lumiere-format-badge {
+ display: inline-block;
+ font-size: 0.65rem;
+ font-weight: 600;
+ letter-spacing: 0.03em;
+ text-transform: uppercase;
+ padding: 0.15em 0.5em;
+ border-radius: 4px;
+ line-height: 1.5;
+ }
+
+ .lumiere-format-badge--latex {
+ background: #e8f5ee;
+ color: #2a9d8f;
+ }
+
+ .lumiere-format-badge--typst {
+ background: #e0f2fe;
+ color: #3d7ebf;
+ }
+
+ .lumiere-format-badge--quarto,
+ .lumiere-format-badge--quarto-slides {
+ background: #ede9fe;
+ color: #7c4dff;
+ }
+}