> ## Documentation Index
> Fetch the complete documentation index at: https://constanza101-borrissol-28.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Gallery page: honeycomb photo grid implementation

> The /gallery page displays a honeycomb tap-to-reveal photo grid of Borrissol workshop creations, served in all 4 languages.

The gallery page showcases participant creations and workshop moments at Borrissol Espai Creatiu. Images are displayed in a honeycomb layout — a CSS `clip-path` grid where diamond-clipped tiles morph into rounded squares on hover or tap, highlighting the selected image while dimming its neighbours. The page is intentionally minimal: a heading, an eyebrow label, and the grid — no sidebar, no filters, no pagination.

## Routes

The gallery is served at four URLs, one per locale. All four are statically pre-rendered at build time:

| Locale            | URL           |
| ----------------- | ------------- |
| Catalan (default) | `/gallery`    |
| Spanish           | `/es/gallery` |
| English           | `/en/gallery` |
| French            | `/fr/gallery` |

The Catalan default route (`gallery.astro`) hardcodes `useTranslations('ca')`. The three non-default locale routes (`[lang]/gallery.astro`) call `useTranslations(lang)` with the locale derived from `getStaticPaths`, resolving the page title, meta description, eyebrow label, and image `alt` text prefix from `src/i18n/ui.ts`.

## SEO metadata

The page title and description are driven by translation keys. In English:

| Key                        | Value                                                                                                                                  |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `page.gallery.title`       | `Gallery`                                                                                                                              |
| `page.gallery.description` | `Photo gallery of Borrissol Espai Creatiu: tufting, punch needle, needle felting, weaving and textile creations in Mataró, Barcelona.` |
| `gallery.eyebrow`          | `Gallery`                                                                                                                              |
| `gallery.headline`         | `Workshops in pictures`                                                                                                                |
| `gallery.image.alt`        | `Tufting and textile techniques workshop at Borrissol Espai Creatiu, Mataró`                                                           |

Each image receives an auto-numbered `alt` attribute: `${t('gallery.image.alt')} 1`, `${t('gallery.image.alt')} 2`, and so on, based on sort order.

## Image pipeline

Gallery images live in `src/assets/images/gallery/` and are processed by the Astro asset pipeline at build time. The `gallery.astro` page uses `import.meta.glob` to load all `.jpg` files eagerly:

```astro theme={null}
const galleryModules = import.meta.glob<{ default: ImageMetadata }>(
  '../assets/images/gallery/*.jpg',
  { eager: true }
);

const images = Object.entries(galleryModules)
  .sort(([a], [b]) => a.localeCompare(b))
  .map(([, module], i) => ({
    src: module.default,
    alt: `${altPrefix} ${i + 1}`,
  }));
```

Images are sorted alphabetically by filename so the display order is deterministic. The sorted array is passed as the `images` prop to `Gallery.astro`.

<Tip>
  To add a new image to the gallery, drop a `.jpg` file into `src/assets/images/gallery/`. Name it with a numeric or alphabetic prefix (e.g. `040-new-piece.jpg`) to control its position in the grid. The Astro build will automatically generate AVIF and WebP variants.
</Tip>

## `Gallery.astro` component

`Gallery.astro` renders the honeycomb grid. It accepts a typed `images` prop:

```typescript theme={null}
interface GalleryImage {
  src: ImageMetadata;
  alt: string;
}

interface Props {
  images: GalleryImage[];
}
```

Inside the component, each image is rendered with `<Picture>` from `astro:assets`, outputting responsive `<source>` elements in AVIF and WebP:

```astro theme={null}
<Picture
  src={src}
  widths={[240, 360, 540, 720]}
  sizes="(max-width: 540px) 40vw, (max-width: 1024px) 22vw, 16vw"
  formats={['avif', 'webp']}
  alt={alt}
  class="gallery-img"
  loading="lazy"
/>
```

All images are `loading="lazy"` — the gallery has no above-the-fold hero image.

## Honeycomb layout mechanics

The interlocking honeycomb pattern is achieved entirely in CSS using a grid + `clip-path: path()` approach. There is no canvas, SVG, or layout library involved.

<CardGroup cols={2}>
  <Card title="Grid structure" icon="grid">
    Each photo cell (`grid-column: span 2`) is a square sized `2 × --cell + gap`. Grid rows are only `--cell` tall, so each cell visually overflows downward by one row. This vertical overflow, combined with the horizontal half-tile offset applied to every `nth-child(7n+5)` cell, produces the honeycomb interlock.
  </Card>

  <Card title="Diamond clip" icon="diamond">
    The initial `clip-path: path(...)` clips each tile to a diamond shape using an 8-curve Bézier. The hover/active state morphs the same path to a rounded square. Both shapes share the same 8-control-point structure so the browser can interpolate smoothly.
  </Card>
</CardGroup>

### Breakpoint grid configuration

The `--cell` CSS custom property controls the tile size at each breakpoint:

| Breakpoint              | Columns | `--cell` | Notes                             |
| ----------------------- | ------- | -------- | --------------------------------- |
| Default (≥ 1367px)      | 8       | `150px`  | Offset pattern: `nth-child(7n+5)` |
| Large laptop (≤ 1366px) | 8       | `130px`  | Same offset pattern               |
| Tablet (≤ 1024px)       | 6       | `110px`  | Offset pattern: `nth-child(5n+4)` |
| Small tablet (≤ 700px)  | 6       | `90px`   | Same 6-col offset                 |
| Mobile (≤ 540px)        | 4       | `80px`   | Offset pattern: `nth-child(3n)`   |

Each breakpoint has its own hardcoded `clip-path: path()` string because CSS `path()` uses absolute pixel coordinates — percentages are not supported — so the path must be rescaled for each tile size.

## Tap-to-reveal interaction

No JavaScript framework is used. The interaction is handled by a small inline script (`is:inline`) injected into the page:

```javascript theme={null}
(function () {
  var grid = document.querySelector('.gallery-grid');
  var tiles = grid.querySelectorAll('.gallery-tile');

  // Click / tap: toggle .is-active on the clicked tile,
  // clear any previously active tile first.
  tiles.forEach(function (btn) {
    btn.addEventListener('click', function (e) {
      e.stopPropagation();
      var wasActive = btn.classList.contains('is-active');
      grid.querySelectorAll('.gallery-tile.is-active').forEach(function (b) {
        b.classList.remove('is-active');
      });
      if (!wasActive) btn.classList.add('is-active');
    });
  });

  // Click outside grid: deactivate all tiles.
  document.addEventListener('click', function (e) {
    if (!grid.contains(e.target)) {
      grid.querySelectorAll('.gallery-tile.is-active').forEach(function (b) {
        b.classList.remove('is-active');
      });
    }
  });

  // Escape key: deactivate all tiles.
  document.addEventListener('keydown', function (e) {
    if (e.key === 'Escape') {
      grid.querySelectorAll('.gallery-tile.is-active').forEach(function (b) {
        b.classList.remove('is-active');
      });
    }
  });
})();
```

The CSS `:has()` selector handles the dimming logic: when any tile has `.is-active`, all *other* tiles receive `filter: brightness(0.55) saturate(0.6)`. The active tile itself gets `filter: brightness(1) saturate(1.15)` and morphs to the rounded-square clip path.

On hover-capable devices (`@media (hover: hover)`), the hover state provides the same morph without requiring a tap. The hover and tap states use identical `clip-path` values.

## Accessibility

* The grid is a `<ul role="list">` with `<li>` cells, making the image count available to screen readers.
* Each tile is a `<button type="button">` with an `aria-label` set to the image's `alt` text.
* `focus-visible` outline is applied to focused tiles for keyboard navigation.
* `@media (prefers-reduced-motion: reduce)` disables the `clip-path` morph transition, leaving only the `filter` transition at a reduced `200ms` duration.

## Page layout

The gallery page uses the shared `Layout.astro` wrapper (which injects GA4, consent mode, skip link, hreflang tags, and Open Graph meta) with `ogType="website"`. It includes `<Navbar>` and `<Footer>` but no `PrivacySection` — the privacy accordion is only included on the home page.
