# Novael

> Bilingual (EN/AR) landing page for a technology services firm: a fixed scroll-spy navbar over a headline that sits on a photographic band, an overlapping intro card and count-up stats, a marquee of past employers, a teal service slab, a project grid whose captions expand on hover, and a contact section that doubles as the footer. Self-contained dark palette, Inter + Manrope with Cairo for Arabic, and RTL that follows the surrounding document.

A shadcn/ui block from the [@hirael](https://hirael.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/hirael/novael).

- Page, with a live preview: https://shadcn.exchange/r/hirael/novael
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/hirael/novael-light-ec3145a8f2201bd3.png
- Registry item: https://hirael.com/r/novael.json
- Categories: templates
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://hirael.com/r/novael.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `button`, `https://hirael.com/r/marquee.json`

## Files

### `registry/hirael/bases/radix/templates/novael/novael.tsx`

```tsx
// Novael from Hirael <https://hirael.com/templates/novael>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import * as React from 'react';

import { cn } from '@/lib/utils';

import { Contact } from './contact';
import { cairo, inter, manrope } from './fonts';
import { Hero } from './hero';
import { Navbar } from './navbar';
import { useDocumentRtl, type Lang } from './primitives';
import { Services } from './services';
import { NovaelStyles } from './styles';
import { Team } from './team';
import { Works } from './works';

const Novael = () => {
  const [langOverride, setLangOverride] = React.useState<Lang | null>(null);
  const docIsRtl = useDocumentRtl();
  const lang: Lang = langOverride ?? (docIsRtl ? 'ar' : 'en');

  return (
    <div
      className={cn(
        'novael',
        inter.variable,
        manrope.variable,
        cairo.variable,
        'flex min-h-svh flex-col bg-background text-foreground antialiased',
      )}
      dir={lang === 'ar' ? 'rtl' : 'ltr'}
      lang={lang}
    >
      <NovaelStyles />
      <Navbar lang={lang} setLang={setLangOverride} />
      <main className="flex-1">
        <Hero lang={lang} />
        <Team lang={lang} />
        <Services lang={lang} />
        <Works lang={lang} />
        <Contact lang={lang} />
      </main>
    </div>
  );
};

export default Novael;
```

### `registry/hirael/bases/radix/templates/novael/navbar.tsx`

```tsx
// Novael from Hirael <https://hirael.com/templates/novael>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import * as React from 'react';
import { Languages, Menu, X } from 'lucide-react';

import { cn } from '@/lib/utils';
import { Button } from '@/registry/hirael/bases/radix/ui/button';

import { SECTION_IDS, useActiveSection, type Lang, type SectionId } from './primitives';

const COPY = {
  en: {
    toLang: 'العربية',
    home: 'Novael home',
    openMenu: 'Open menu',
    closeMenu: 'Close menu',
    menuTitle: 'Lorem ipsum',
    nav: { intro: 'Lorem', services: 'Ipsum dolor', works: 'Sit amet', contact: 'Consectetur' },
  },
  ar: {
    toLang: 'English',
    home: 'نوفايل، الرئيسية',
    openMenu: 'افتح القائمة',
    closeMenu: 'أغلق القائمة',
    menuTitle: 'لوريم إيبسوم',
    nav: { intro: 'لوريم', services: 'إيبسوم دولور', works: 'سيت أميت', contact: 'كونسيكتيتور' },
  },
} satisfies Record<Lang, { nav: Record<SectionId, string> } & Record<string, unknown>>;

const Wordmark = () => {
  return (
    <span data-slot="novael-display" className="text-lg font-semibold tracking-[0.25em] text-foreground">
      <span className="text-primary">N</span>OVAEL
    </span>
  );
};

interface NavbarProps {
  lang: Lang;
  setLang: (lang: Lang) => void;
}

export const Navbar = ({ lang, setLang }: NavbarProps) => {
  const active = useActiveSection(SECTION_IDS);
  const [open, setOpen] = React.useState(false);
  const c = COPY[lang];

  return (
    <>
      <header className="fixed inset-x-0 top-0 z-50 h-18 border-b border-border bg-background/90 backdrop-blur-sm">
        <div className="relative mx-auto flex h-full max-w-[1400px] items-center px-6">
          <a href="#intro" aria-label={c.home} className="relative z-10 inline-flex items-center">
            <Wordmark />
          </a>

          <nav className="absolute start-1/2 hidden h-full -translate-x-1/2 md:block rtl:translate-x-1/2">
            <ul className="flex h-full items-stretch border-s border-border">
              {SECTION_IDS.map((id) => (
                <li key={id} className="border-e border-border">
                  <a
                    href={`#${id}`}
                    aria-current={active === id ? 'true' : undefined}
                    className={cn(
                      'flex h-full items-center px-7 text-[10px] font-medium tracking-[0.3em] uppercase transition-colors',
                      'rtl:text-sm rtl:tracking-normal',
                      active === id ? 'bg-accent text-accent-foreground' : 'text-foreground/50 hover:text-foreground',
                    )}
                  >
                    {c.nav[id]}
                  </a>
                </li>
              ))}
            </ul>
          </nav>

          <div className="ms-auto flex items-center gap-1">
            <Button type="button" variant="ghost" size="sm" onClick={() => setLang(lang === 'en' ? 'ar' : 'en')}>
              <Languages className="size-4" />
              <span className="hidden text-sm font-medium sm:inline">{c.toLang}</span>
            </Button>

            <Button
              type="button"
              variant="ghost"
              size="icon"
              aria-label={c.openMenu}
              aria-expanded={open}
              onClick={() => setOpen(true)}
              className="md:hidden"
            >
              <Menu className="size-5" />
            </Button>
          </div>
        </div>
      </header>

      {/* Outside the header: its backdrop-filter would make it the containing block for these fixed layers,
          squeezing the drawer into the header's height. */}
      <div
        aria-hidden={!open}
        className={cn(
          'fixed inset-0 z-50 bg-black/50 transition-opacity duration-300 md:hidden',
          open ? 'opacity-100' : 'pointer-events-none opacity-0',
        )}
        onClick={() => setOpen(false)}
      />
      <div
        role="dialog"
        aria-modal={open}
        aria-label={c.menuTitle}
        // Closed, the drawer still sits past the viewport edge; inert and invisible take it out of focus order,
        // the accessibility tree and hit testing. visibility is transitioned so the slide-out still plays.
        inert={!open}
        className={cn(
          'fixed inset-y-0 end-0 z-50 flex w-72 max-w-[80vw] flex-col border-s border-border bg-background transition-[translate,visibility] duration-300 md:hidden',
          open ? 'visible translate-x-0' : 'invisible translate-x-full rtl:-translate-x-full',
        )}
      >
        <div className="flex h-18 items-center justify-between border-b border-border px-5">
          <span data-slot="novael-display" className="text-lg font-medium text-foreground">
            {c.menuTitle}
          </span>
          <Button type="button" variant="ghost" size="icon" aria-label={c.closeMenu} onClick={() => setOpen(false)}>
            <X className="size-5" />
          </Button>
        </div>
        <nav className="flex flex-col px-5">
          {SECTION_IDS.map((id) => (
            <a
              key={id}
              href={`#${id}`}
              onClick={() => setOpen(false)}
              data-slot="novael-display"
              className={cn(
                'border-b border-border/60 py-4 text-lg font-light transition-colors',
                active === id ? 'text-primary' : 'text-foreground hover:text-primary',
              )}
            >
              {c.nav[id]}
            </a>
          ))}
        </nav>
      </div>
    </>
  );
};
```

### `registry/hirael/bases/radix/templates/novael/hero.tsx`

```tsx
// Novael from Hirael <https://hirael.com/templates/novael>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import Image from 'next/image';
import { ArrowDown } from 'lucide-react';

import { LOREM, CountUp, Reveal, type Lang } from './primitives';

const BACKDROP = 'https://hirael.com/media/templates/novael/intro-bg.jpg';

const STATS = [
  { id: 'experience', value: 17, suffix: '+' },
  { id: 'hours', value: 40, suffix: '+' },
  { id: 'projects', value: 20, suffix: '+' },
] as const;

const COPY = {
  en: {
    scrollDown: 'Lorem ipsum',
    headline: 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor',
    aboutTitle: 'Lorem ipsum dolor sit.',
    aboutText: LOREM.en.long,
    stats: {
      experience: {
        title: 'Lorem ipsum',
        text: LOREM.en.medium,
      },
      hours: {
        title: 'Dolor sit amet',
        text: LOREM.en.medium,
      },
      projects: {
        title: 'Consectetur adipiscing',
        text: LOREM.en.medium,
      },
    },
  },
  ar: {
    scrollDown: 'لوريم إيبسوم',
    headline: 'لوريم إيبسوم دولور سيت أميت كونسيكتيتور أديبيسكينج إليت سيد دو إيوسمود',
    aboutTitle: 'لوريم إيبسوم دولور سيت.',
    aboutText: LOREM.ar.long,
    stats: {
      experience: {
        title: 'لوريم إيبسوم',
        text: LOREM.ar.medium,
      },
      hours: {
        title: 'دولور سيت أميت',
        text: LOREM.ar.medium,
      },
      projects: {
        title: 'كونسيكتيتور أديبيسكينج',
        text: LOREM.ar.medium,
      },
    },
  },
} satisfies Record<Lang, unknown>;

interface HeroProps {
  lang: Lang;
}

export const Hero = ({ lang }: HeroProps) => {
  const c = COPY[lang];

  return (
    <section id="intro" className="relative overflow-hidden pt-28 pb-16 sm:pt-36 sm:pb-24">
      <div className="mx-auto max-w-[1400px] px-6">
        <div className="relative mx-auto max-w-5xl px-[4%] pb-40 sm:pb-56">
          <div className="absolute inset-x-0 top-[42%] bottom-0 -z-10 overflow-hidden">
            <Image src={BACKDROP} alt="" fill priority sizes="100vw" className="object-cover" />
            <div aria-hidden className="absolute inset-0 bg-black/40" />
          </div>

          <h1
            data-slot="novael-display"
            className="text-center text-3xl leading-[1.15] font-medium text-balance text-foreground sm:text-4xl md:text-5xl lg:text-6xl xl:text-7xl"
          >
            {c.headline}
            <span className="text-primary">.</span>
          </h1>

          <a
            href="#services"
            className="absolute end-0 bottom-24 hidden origin-bottom-right rotate-90 items-center gap-3 text-[10px] font-light tracking-[0.5em] text-foreground/70 uppercase transition-colors hover:text-primary lg:flex rtl:text-xs rtl:tracking-normal"
          >
            <span>{c.scrollDown}</span>
            <ArrowDown className="size-3.5 rotate-90" />
          </a>
        </div>

        <Reveal className="relative z-10 mx-auto -mt-24 w-full max-w-3xl sm:-mt-28">
          <span aria-hidden className="absolute start-1/2 top-0 h-8 w-px -translate-y-full bg-primary" />
          <div className="border border-border/60 bg-card/85 px-7 py-10 text-center backdrop-blur-sm sm:px-12 sm:py-12">
            <h2 data-slot="novael-display" className="mb-6 text-xl font-medium text-foreground sm:text-2xl">
              {c.aboutTitle}
            </h2>
            <p className="leading-relaxed font-light text-muted-foreground">{c.aboutText}</p>
          </div>
        </Reveal>

        <div className="mx-auto mt-20 grid max-w-5xl gap-10 sm:grid-cols-3 sm:gap-8">
          {STATS.map((stat, index) => (
            <Reveal key={stat.id} delay={index * 150} className="text-center sm:text-start">
              <div className="text-5xl leading-none font-medium text-primary sm:text-6xl">
                <CountUp to={stat.value} suffix={stat.suffix} />
              </div>
              <h3 className="mt-4 text-lg font-medium text-foreground">{c.stats[stat.id].title}</h3>
              <p className="mt-2 text-sm leading-relaxed font-light text-muted-foreground">{c.stats[stat.id].text}</p>
            </Reveal>
          ))}
        </div>
      </div>
    </section>
  );
};
```

### `registry/hirael/bases/radix/templates/novael/team.tsx`

```tsx
// Novael from Hirael <https://hirael.com/templates/novael>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import { cn } from '@/lib/utils';
import { Marquee } from '@/registry/hirael/bases/radix/components/marquee';

import { LOREM, Reveal, SectionHeader, type Lang } from './primitives';

const COMPANIES = ['Halcyon', 'Cobalt', 'Lumen Labs', 'Vantara', 'Orbital', 'Ardent', 'Solstice', 'Kestrel'] as const;

const COPY = {
  en: {
    pretitle: 'Lorem ipsum',
    title: 'Lorem ipsum dolor sit amet',
    lead: LOREM.en.long,
    companiesLabel: 'Consectetur adipiscing',
  },
  ar: {
    pretitle: 'لوريم إيبسوم',
    title: 'لوريم إيبسوم دولور سيت أميت',
    lead: LOREM.ar.long,
    companiesLabel: 'كونسيكتيتور أديبيسكينج',
  },
} satisfies Record<Lang, unknown>;

interface TeamProps {
  lang: Lang;
}

export const Team = ({ lang }: TeamProps) => {
  const c = COPY[lang];

  return (
    <section id="team" className="py-20 sm:py-28">
      <div className="mx-auto max-w-[1440px] px-6">
        <SectionHeader lang={lang} pretitle={c.pretitle} title={c.title} />

        <Reveal className="mx-auto mt-10 max-w-2xl text-center">
          <p className="text-lg leading-relaxed font-light text-muted-foreground">{c.lead}</p>
        </Reveal>

        <Reveal className="mt-16 border-t border-border/60 pt-12">
          <p
            className={cn(
              'text-center text-xs font-medium text-muted-foreground',
              lang === 'en' ? 'tracking-[0.3em] uppercase' : 'text-sm tracking-normal',
            )}
          >
            {c.companiesLabel}
          </p>
          <div dir="ltr" className="relative mt-7 w-full min-w-0 overflow-hidden">
            <Marquee pauseOnHover duration={20} gap="2rem">
              {COMPANIES.map((company) => (
                <span
                  key={company}
                  data-slot="novael-display"
                  className="text-xl font-semibold tracking-wide whitespace-nowrap text-foreground/70 transition-colors hover:text-foreground sm:text-2xl"
                >
                  {company}
                </span>
              ))}
            </Marquee>
            <div
              aria-hidden
              className="pointer-events-none absolute inset-y-0 left-0 w-16 bg-gradient-to-r from-background to-transparent"
            />
            <div
              aria-hidden
              className="pointer-events-none absolute inset-y-0 right-0 w-16 bg-gradient-to-l from-background to-transparent"
            />
          </div>
        </Reveal>
      </div>
    </section>
  );
};
```

### `registry/hirael/bases/radix/templates/novael/services.tsx`

```tsx
// Novael from Hirael <https://hirael.com/templates/novael>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import {
  CodeXml,
  Infinity as InfinityIcon,
  Network,
  Server,
  Smartphone,
  TestTubeDiagonal,
  type LucideIcon,
} from 'lucide-react';

import { LOREM, Reveal, SectionHeader, type Lang } from './primitives';

const SERVICES = [
  { id: 'frontend', icon: CodeXml },
  { id: 'backend', icon: Server },
  { id: 'mobile', icon: Smartphone },
  { id: 'qa', icon: TestTubeDiagonal },
  { id: 'devops', icon: InfinityIcon },
  { id: 'infra', icon: Network },
] as const satisfies readonly { id: string; icon: LucideIcon }[];

type ServiceId = (typeof SERVICES)[number]['id'];

const COPY = {
  en: {
    pretitle: 'Ipsum dolor',
    title: 'Lorem ipsum dolor sit amet consectetur adipiscing elit',
    items: {
      frontend: {
        title: 'Lorem ipsum',
        text: LOREM.en.medium,
      },
      backend: {
        title: 'Dolor sit amet',
        text: LOREM.en.medium,
      },
      mobile: {
        title: 'Consectetur adipiscing',
        text: LOREM.en.medium,
      },
      qa: {
        title: 'Sed eiusmod',
        text: LOREM.en.medium,
      },
      devops: {
        title: 'Tempor incididunt',
        text: LOREM.en.medium,
      },
      infra: {
        title: 'Ut labore et dolore',
        text: LOREM.en.medium,
      },
    },
  },
  ar: {
    pretitle: 'إيبسوم دولور',
    title: 'لوريم إيبسوم دولور سيت أميت كونسيكتيتور أديبيسكينج إليت',
    items: {
      frontend: {
        title: 'لوريم إيبسوم',
        text: LOREM.ar.medium,
      },
      backend: {
        title: 'دولور سيت أميت',
        text: LOREM.ar.medium,
      },
      mobile: {
        title: 'كونسيكتيتور أديبيسكينج',
        text: LOREM.ar.medium,
      },
      qa: {
        title: 'سيد إيوسمود',
        text: LOREM.ar.medium,
      },
      devops: {
        title: 'تيمبور إنسيديدنت',
        text: LOREM.ar.medium,
      },
      infra: {
        title: 'أوت لابوري إت دولوري',
        text: LOREM.ar.medium,
      },
    },
  },
} satisfies Record<Lang, { items: Record<ServiceId, { title: string; text: string }> } & Record<string, unknown>>;

interface ServicesProps {
  lang: Lang;
}

export const Services = ({ lang }: ServicesProps) => {
  const c = COPY[lang];

  return (
    <section id="services" className="bg-(--novael-panel) py-20 sm:py-28">
      <div className="mx-auto max-w-[1440px] px-6">
        <SectionHeader lang={lang} tone="panel" pretitle={c.pretitle} title={c.title} />

        <div className="mt-14 grid gap-x-12 gap-y-10 md:grid-cols-2">
          {SERVICES.map((service, index) => {
            const Icon = service.icon;
            const copy = c.items[service.id];

            return (
              <Reveal key={service.id} delay={(index % 2) * 120} className="flex gap-6">
                <div className="shrink-0 pt-1 text-(--novael-panel-foreground)">
                  <Icon className="size-7" strokeWidth={1.5} />
                </div>
                <div>
                  <h3 data-slot="novael-display" className="text-xl font-semibold text-(--novael-panel-foreground)">
                    {copy.title}
                  </h3>
                  <p className="mt-2 leading-relaxed font-light text-(--novael-panel-foreground)/85">{copy.text}</p>
                </div>
              </Reveal>
            );
          })}
        </div>
      </div>
    </section>
  );
};
```

### `registry/hirael/bases/radix/templates/novael/works.tsx`

```tsx
// Novael from Hirael <https://hirael.com/templates/novael>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import { ArrowUpRight } from 'lucide-react';

import { cn } from '@/lib/utils';

import { LOREM, Reveal, SectionHeader, type Lang } from './primitives';

const PROJECT_IDS = ['northwind', 'pipeline', 'cascade', 'bazaar', 'meridian', 'ledger', 'atlas', 'playfield'] as const;

type ProjectId = (typeof PROJECT_IDS)[number];

const COPY = {
  en: {
    pretitle: 'Sit amet',
    title: 'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor.',
    projectLink: 'Lorem ipsum',
    items: {
      northwind: {
        title: 'Lorem Ipsum',
        category: 'Lorem',
        description: LOREM.en.medium,
      },
      pipeline: {
        title: 'Dolor Sit',
        category: 'Ipsum',
        description: LOREM.en.medium,
      },
      cascade: {
        title: 'Amet Consectetur',
        category: 'Sed eiusmod',
        description: LOREM.en.medium,
      },
      bazaar: {
        title: 'Adipiscing Elit',
        category: 'Amet',
        description: LOREM.en.medium,
      },
      meridian: {
        title: 'Sed Eiusmod',
        category: 'Consectetur',
        description: LOREM.en.medium,
      },
      ledger: {
        title: 'Tempor Incididunt',
        category: 'Adipiscing',
        description: LOREM.en.medium,
      },
      atlas: {
        title: 'Ut Labore',
        category: 'Sed eiusmod',
        description: LOREM.en.medium,
      },
      playfield: {
        title: 'Et Dolore',
        category: 'Elit',
        description: LOREM.en.medium,
      },
    },
  },
  ar: {
    pretitle: 'سيت أميت',
    title: 'لوريم إيبسوم دولور سيت أميت كونسيكتيتور أديبيسكينج إليت سيد دو إيوسمود.',
    projectLink: 'لوريم إيبسوم',
    items: {
      northwind: {
        title: 'لوريم إيبسوم',
        category: 'لوريم',
        description: LOREM.ar.medium,
      },
      pipeline: {
        title: 'دولور سيت',
        category: 'إيبسوم',
        description: LOREM.ar.medium,
      },
      cascade: {
        title: 'أميت كونسيكتيتور',
        category: 'سيد إيوسمود',
        description: LOREM.ar.medium,
      },
      bazaar: {
        title: 'أديبيسكينج إليت',
        category: 'أميت',
        description: LOREM.ar.medium,
      },
      meridian: {
        title: 'سيد إيوسمود',
        category: 'كونسيكتيتور',
        description: LOREM.ar.medium,
      },
      ledger: {
        title: 'تيمبور إنسيديدنت',
        category: 'أديبيسكينج',
        description: LOREM.ar.medium,
      },
      atlas: {
        title: 'أوت لابوري',
        category: 'سيد إيوسمود',
        description: LOREM.ar.medium,
      },
      playfield: {
        title: 'إت دولوري',
        category: 'إليت',
        description: LOREM.ar.medium,
      },
    },
  },
} satisfies Record<
  Lang,
  { items: Record<ProjectId, { title: string; category: string; description: string }> } & Record<string, unknown>
>;

interface ProjectPlaceholderProps {
  index: number;
}

const ProjectPlaceholder = ({ index }: ProjectPlaceholderProps) => {
  return (
    <div
      aria-hidden
      data-slot="project-placeholder"
      className="absolute inset-0 bg-secondary"
      style={{
        backgroundImage: [
          `radial-gradient(120% 90% at ${index % 2 === 0 ? '15%' : '85%'} 0%, color-mix(in oklch, var(--primary) 26%, transparent), transparent 62%)`,
          'linear-gradient(160deg, color-mix(in oklch, var(--foreground) 7%, transparent), transparent 55%)',
        ].join(', '),
      }}
    >
      <span className="absolute end-5 bottom-4 text-6xl font-medium text-foreground/10 tabular-nums sm:text-7xl">
        {String(index + 1).padStart(2, '0')}
      </span>
    </div>
  );
};

interface WorksProps {
  lang: Lang;
}

export const Works = ({ lang }: WorksProps) => {
  const c = COPY[lang];

  return (
    <section id="works" className="py-20 sm:py-28">
      <div className="mx-auto max-w-[1440px] px-6">
        <SectionHeader lang={lang} pretitle={c.pretitle} title={c.title} />

        <div className="mt-14 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
          {PROJECT_IDS.map((id, index) => {
            const copy = c.items[id];

            return (
              <Reveal key={id} delay={(index % 2) * 120}>
                <a
                  href="#"
                  aria-label={`${copy.title}, ${copy.category}`}
                  className="group relative block overflow-hidden rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
                >
                  <div className="relative aspect-[16/10] w-full overflow-hidden bg-card">
                    <ProjectPlaceholder index={index} />
                    <div
                      aria-hidden
                      className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/30 to-transparent"
                    />
                    <div
                      aria-hidden
                      className="absolute inset-0 bg-black/55 opacity-0 transition-opacity duration-500 group-hover:opacity-100 group-focus-visible:opacity-100"
                    />
                  </div>

                  <div className="absolute inset-x-0 bottom-0 p-6 sm:p-7">
                    <p
                      className={cn(
                        'text-xs font-medium text-primary',
                        lang === 'en' ? 'tracking-[0.2em] uppercase' : 'text-sm tracking-normal',
                      )}
                    >
                      {copy.category}
                    </p>
                    <h3 data-slot="novael-display" className="mt-1 text-xl font-medium text-white sm:text-2xl">
                      {copy.title}
                    </h3>

                    <div className="grid grid-rows-[0fr] transition-[grid-template-rows] duration-500 ease-out group-hover:grid-rows-[1fr] group-focus-visible:grid-rows-[1fr]">
                      <div className="overflow-hidden">
                        <p className="pt-3 text-sm leading-relaxed font-light text-white/70">{copy.description}</p>
                      </div>
                    </div>

                    <span className="mt-4 inline-flex translate-y-1 items-center gap-2 text-xs font-medium tracking-wide text-white opacity-0 transition-all duration-500 ease-out group-hover:translate-y-0 group-hover:opacity-100 group-focus-visible:translate-y-0 group-focus-visible:opacity-100 rtl:tracking-normal">
                      {c.projectLink}
                      <ArrowUpRight className="size-3.5 rtl:-scale-x-100" />
                    </span>
                  </div>
                </a>
              </Reveal>
            );
          })}
        </div>
      </div>
    </section>
  );
};
```

### `registry/hirael/bases/radix/templates/novael/contact.tsx`

```tsx
// Novael from Hirael <https://hirael.com/templates/novael>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import { ArrowUp, Mail } from 'lucide-react';

import { cn } from '@/lib/utils';
import { Button } from '@/registry/hirael/bases/radix/ui/button';

import { LOREM, Reveal, SectionHeader, type Lang } from './primitives';

const FOOTER_HEADING = 'mb-4 text-base font-semibold text-foreground';

const CONTACT = {
  email: 'hello@novael.example',
  linkedin: '#',
} as const;

const COPY = {
  en: {
    pretitle: 'Consectetur',
    title: 'Lorem ipsum dolor sit amet',
    lead: LOREM.en.long,
    cta: 'Lorem ipsum',
    findUs: 'Dolor sit amet',
    address: ['Lorem ipsum dolor 20', 'Sit amet, CA 90210', 'Consectetur adipiscing'],
    followUs: 'Sed eiusmod',
    linkedin: 'Tempor',
    contactUs: 'Incididunt',
    copyright: '© Copyright Novael, LLC',
    backToTop: 'Ut labore',
  },
  ar: {
    pretitle: 'كونسيكتيتور',
    title: 'لوريم إيبسوم دولور سيت أميت',
    lead: LOREM.ar.long,
    cta: 'لوريم إيبسوم',
    findUs: 'دولور سيت أميت',
    address: ['لوريم إيبسوم دولور ٢٠', 'سيت أميت، ٩٠٢١٠', 'كونسيكتيتور أديبيسكينج'],
    followUs: 'سيد إيوسمود',
    linkedin: 'تيمبور',
    contactUs: 'إنسيديدنت',
    copyright: '© جميع الحقوق محفوظة نوفايل',
    backToTop: 'أوت لابوري',
  },
} satisfies Record<Lang, unknown>;

interface ContactProps {
  lang: Lang;
}

export const Contact = ({ lang }: ContactProps) => {
  const c = COPY[lang];
  const year = new Date().getFullYear();

  return (
    <section id="contact" className="bg-card pt-20 pb-10 sm:pt-28">
      <div className="mx-auto max-w-[1440px] px-6">
        <SectionHeader lang={lang} pretitle={c.pretitle} title={c.title} />

        <Reveal className="mx-auto mt-10 max-w-2xl text-center">
          <p className="text-lg leading-relaxed font-light text-muted-foreground">{c.lead}</p>
          <div className="mt-10">
            <Button asChild size="lg" className="h-12">
              <a href={`mailto:${CONTACT.email}`}>
                <Mail className="size-4" />
                <span className={cn(lang === 'en' ? 'text-xs tracking-[0.2em] uppercase' : 'text-sm')}>{c.cta}</span>
              </a>
            </Button>
          </div>
        </Reveal>

        <footer className="mt-24">
          <div className="grid gap-10 border-t border-border/60 pt-12 sm:grid-cols-2 lg:grid-cols-3">
            <div>
              <h3 data-slot="novael-display" className={FOOTER_HEADING}>
                {c.findUs}
              </h3>
              <address className="leading-relaxed font-light text-muted-foreground not-italic">
                {c.address.map((line) => (
                  <span key={line} className="block">
                    {line}
                  </span>
                ))}
              </address>
            </div>

            <div>
              <h3 data-slot="novael-display" className={FOOTER_HEADING}>
                {c.followUs}
              </h3>
              <ul className="flex flex-col gap-1">
                <li>
                  <a href={CONTACT.linkedin} className="text-foreground transition-colors hover:text-primary">
                    {c.linkedin}
                  </a>
                </li>
              </ul>
            </div>

            <div>
              <h3 data-slot="novael-display" className={FOOTER_HEADING}>
                {c.contactUs}
              </h3>
              <ul className="flex flex-col gap-1">
                <li>
                  <a href={`mailto:${CONTACT.email}`} className="text-foreground transition-colors hover:text-primary">
                    {CONTACT.email}
                  </a>
                </li>
              </ul>
            </div>
          </div>

          <div className="mt-12 flex flex-col items-center justify-between gap-6 sm:flex-row">
            <span className="text-sm text-muted-foreground">
              {c.copyright} {year}
            </span>
            <a
              href="#intro"
              className={cn(
                'group inline-flex items-center gap-3 font-light text-foreground',
                lang === 'en' ? 'text-[10px] tracking-[0.3em] uppercase' : 'text-xs tracking-normal',
              )}
            >
              <span className="flex size-9 items-center justify-center rounded-full border-2 border-primary">
                <ArrowUp className="size-4 text-primary transition-transform group-hover:-translate-y-0.5" />
              </span>
              {c.backToTop}
            </a>
          </div>
        </footer>
      </div>
    </section>
  );
};
```

### `registry/hirael/bases/radix/templates/novael/primitives.tsx`

```tsx
// Novael from Hirael <https://hirael.com/templates/novael>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

'use client';

import * as React from 'react';

import { cn } from '@/lib/utils';

export type Lang = 'en' | 'ar';

export const SECTION_IDS = ['intro', 'services', 'works', 'contact'] as const;

export type SectionId = (typeof SECTION_IDS)[number];

export const LOREM: Record<Lang, { short: string; medium: string; long: string }> = {
  en: {
    short: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
    medium:
      'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
    long: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
  },
  ar: {
    short: 'لوريم إيبسوم دولور سيت أميت، كونسيكتيتور أديبيسكينج إليت.',
    medium:
      'لوريم إيبسوم دولور سيت أميت، كونسيكتيتور أديبيسكينج إليت. سيد دو إيوسمود تيمبور إنسيديدنت أوت لابوري إت دولوري ماجنا أليكوا.',
    long: 'لوريم إيبسوم دولور سيت أميت، كونسيكتيتور أديبيسكينج إليت. سيد دو إيوسمود تيمبور إنسيديدنت أوت لابوري إت دولوري ماجنا أليكوا. أوت إينيم أد مينيم فينيام، كويس نوستريد إكسير سيتاشن أولامكو لابوريس نيسي.',
  },
};

export const useDocumentRtl = () => {
  const subscribe = React.useCallback(() => () => {}, []);

  return React.useSyncExternalStore(
    subscribe,
    () => document.documentElement.getAttribute('dir') === 'rtl',
    () => false,
  );
};

export const useActiveSection = (ids: readonly string[]) => {
  const [active, setActive] = React.useState<string>(ids[0]);

  React.useEffect(() => {
    const sections = ids.map((id) => document.getElementById(id)).filter((el): el is HTMLElement => el !== null);
    if (sections.length === 0) return;

    const observer = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          if (entry.isIntersecting) setActive(entry.target.id);
        }
      },
      { rootMargin: '-45% 0px -50% 0px', threshold: 0 },
    );

    sections.forEach((section) => observer.observe(section));

    return () => observer.disconnect();
  }, [ids]);

  return active;
};

interface RevealProps extends React.ComponentProps<'div'> {
  delay?: number;
}

export const Reveal = ({ className, delay = 0, children, ...props }: RevealProps) => {
  const ref = React.useRef<HTMLDivElement | null>(null);
  const [shown, setShown] = React.useState(false);

  React.useEffect(() => {
    const node = ref.current;
    if (!node) return;

    const observer = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          if (entry.isIntersecting) {
            setShown(true);
            observer.disconnect();
            break;
          }
        }
      },
      { threshold: 0.15, rootMargin: '0px 0px -10% 0px' },
    );

    observer.observe(node);

    return () => observer.disconnect();
  }, []);

  return (
    <div
      ref={ref}
      style={{ transitionDelay: shown ? `${delay}ms` : '0ms' }}
      className={cn(
        'transition-all duration-700 ease-out will-change-transform motion-reduce:transition-none',
        shown ? 'translate-y-0 opacity-100' : 'translate-y-12 opacity-0',
        className,
      )}
      {...props}
    >
      {children}
    </div>
  );
};

interface CountUpProps {
  to: number;
  suffix?: string;
  durationMs?: number;
  className?: string;
}

export const CountUp = ({ to, suffix = '', durationMs = 2000, className }: CountUpProps) => {
  const ref = React.useRef<HTMLSpanElement | null>(null);
  const [value, setValue] = React.useState(0);

  React.useEffect(() => {
    const node = ref.current;
    if (!node) return;

    const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    let raf = 0;
    let start: number | null = null;

    const step = (timestamp: number) => {
      if (start === null) start = timestamp;
      const progress = Math.min((timestamp - start) / durationMs, 1);
      setValue(Math.round(progress * to));
      if (progress < 1) raf = requestAnimationFrame(step);
    };

    const observer = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          if (entry.isIntersecting) {
            if (prefersReduced) setValue(to);
            else raf = requestAnimationFrame(step);
            observer.disconnect();
            break;
          }
        }
      },
      { threshold: 0.4 },
    );

    observer.observe(node);

    return () => {
      observer.disconnect();
      cancelAnimationFrame(raf);
    };
  }, [to, durationMs]);

  return (
    <span ref={ref} className={className}>
      {value}
      {suffix}
    </span>
  );
};

type SectionHeaderTone = 'surface' | 'panel';

interface SectionHeaderProps {
  pretitle: string;
  title: React.ReactNode;
  lang: Lang;
  tone?: SectionHeaderTone;
  className?: string;
}

export const SectionHeader = ({ pretitle, title, lang, tone = 'surface', className }: SectionHeaderProps) => {
  const onPanel = tone === 'panel';

  return (
    <div className={cn('relative mx-auto max-w-3xl pb-10 text-center', className)}>
      <p
        data-slot="novael-display"
        className={cn(
          'text-sm font-semibold',
          lang === 'en' ? 'tracking-[0.3em] uppercase' : 'tracking-normal',
          onPanel ? 'text-(--novael-panel-muted)' : 'text-primary',
        )}
      >
        {pretitle}
      </p>
      <h2
        data-slot="novael-display"
        className={cn(
          'mt-4 text-[2rem] leading-tight font-semibold text-balance sm:text-4xl md:text-5xl',
          onPanel ? 'text-(--novael-panel-foreground)' : 'text-foreground',
        )}
      >
        {title}
      </h2>
      <span
        aria-hidden
        className={cn(
          'absolute start-1/2 bottom-0 h-px w-80 max-w-[80%] -translate-x-1/2 rtl:translate-x-1/2',
          onPanel ? 'bg-black/15' : 'bg-foreground/15',
        )}
      />
    </div>
  );
};
```

### `registry/hirael/bases/radix/templates/novael/styles.tsx`

```tsx
// Novael from Hirael <https://hirael.com/templates/novael>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

const NOVAEL_CSS = `
.novael {
  --radius: 0.375rem;
  --background: hsl(210 5% 7%);
  --foreground: hsl(0 0% 91%);
  --card: hsl(180 3% 5%);
  --card-foreground: hsl(0 0% 91%);
  --popover: hsl(200 4% 8%);
  --popover-foreground: hsl(0 0% 91%);
  --primary: hsl(174 65% 40%);
  --primary-foreground: hsl(0 0% 100%);
  --secondary: hsl(210 3% 11%);
  --secondary-foreground: hsl(0 0% 91%);
  --muted: hsl(210 3% 10%);
  --muted-foreground: hsl(240 1% 54%);
  --accent: hsl(240 1% 17%);
  --accent-foreground: hsl(0 0% 100%);
  --destructive: hsl(0 70% 60%);
  --destructive-foreground: hsl(0 0% 100%);
  --border: hsl(0 0% 100% / 10%);
  --input: hsl(0 0% 100% / 13%);
  --ring: hsl(174 65% 50%);
  --novael-panel: hsl(174 65% 34%);
  --novael-panel-foreground: hsl(0 0% 100%);
  --novael-panel-muted: hsl(0 0% 0% / 55%);
}

.novael {
  --novael-sans: var(--font-novael-sans), ui-sans-serif, system-ui, sans-serif;
  --novael-display: var(--font-novael-display), ui-sans-serif, system-ui, sans-serif;
  font-family: var(--novael-sans);
}

.novael:lang(ar) {
  --novael-sans: var(--font-novael-arabic), ui-sans-serif, system-ui, sans-serif;
  --novael-display: var(--font-novael-arabic), ui-sans-serif, system-ui, sans-serif;
}

.novael [data-slot='novael-display'] {
  font-family: var(--novael-display);
}
`;

export const NovaelStyles = () => {
  return <style dangerouslySetInnerHTML={{ __html: NOVAEL_CSS }} />;
};
```

### `registry/hirael/bases/radix/templates/novael/fonts.ts`

```ts
// Novael from Hirael <https://hirael.com/templates/novael>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael

import { Cairo, Inter, Manrope } from 'next/font/google';

export const inter = Inter({
  variable: '--font-novael-sans',
  subsets: ['latin'],
  display: 'swap',
});

export const manrope = Manrope({
  variable: '--font-novael-display',
  subsets: ['latin'],
  display: 'swap',
});

export const cairo = Cairo({
  variable: '--font-novael-arabic',
  subsets: ['arabic', 'latin'],
  display: 'swap',
});
```
