# Assembly — ideas meet, hands make

Build a complete, original design-conference website named Assembly. The slug was checked against the existing source and generated-demo directories and is unused. The site should be bold, graphic, and editorial, with oversized typography, original geometric poster systems, substantial fictional speaker profiles, a two-day programme, a personal itinerary with explicit overlap handling, portable exports, and an honest venue and access guide. It must be distinct from a concert or nightlife festival: the content concerns design talks, practical workshops, and creative conversations.

Assembly is a fictional conference. All speakers, practices, projects, sessions, dates, and venue details are invented for this demonstration. No actual tickets, registrations, reservations, invitations, or physical services are offered. The site should still feel complete as a product: visitors can explore the programme, understand the speakers’ ideas, choose sessions, resolve scheduling conflicts, and export a useful local plan. The interaction should do real work without pretending to connect to a real event operation.

## Live reference research

The live reference is OFFF Barcelona at https://www.offf.barcelona/. The homepage, lineup, schedule destination, and an individual lineup detail were inspected. The homepage uses a full-width motion-led identity with very large letterforms and a compact multicolor navigation grid. Each navigation cell has a distinct flat color, while a large ticket action occupies a stronger black area. This gives the site the visual energy of a festival poster before the visitor reaches the programme.

The lineup page was inspected as an information-rich table. It uses numbered entries, names, locations, disciplines, strong horizontal rules, a search field, and location and field filters. The page is not simply a collection of portrait cards. It makes the range of practices legible through direct typography and structure. Assembly should borrow that confidence in information hierarchy, while choosing a different original identity and a more explicit local planning interaction.

The Koto detail at https://www.offf.barcelona/lineup/koto was opened from the live lineup. It has a strong title, colored discipline chips, short biography columns, external practice links, and broad work imagery. The measured title was approximately 52.9 pixels with a 45.5-pixel line height at a 1471-pixel viewport. The important transferable relationship is between a practitioner’s point of view and examples of their work. Assembly’s speaker pages therefore include original project studies and substantial explanations rather than only a name and session time.

The homepage’s workshop link pointed to an older schedule URL. Opening it redirected to the 2027 schedule, which remained in a loading state during the inspection. The current page exposed type and date filter controls, but populated session timings were not verified. Do not invent reference observations about its schedule results. Assembly’s dates, timing rules, filters, and overlap interactions are original implementation decisions. Record the reference limitation plainly rather than claiming that a complete schedule interaction was observed.

Do not reuse OFFF’s logo, custom typeface, promotional footage, speaker names, biographies, project imagery, sponsor marks, dates, venue, or source code. The reference inspires the scale, graphic confidence, and connection between programme and practice. Assembly’s complete content and visual system are independently authored. No actual OFFF tickets or services should be linked from the demo’s calls to action.

## Original identity

Use a deliberately limited, high-contrast palette: vermilion around #f05c3b, warm cream around #f8f5e9, charcoal around #20221d, sulfur yellow around #ddd653, and cobalt-violet around #5148d7. The colors should appear as solid fields, not gradients. Black rules and direct typography hold the system together. The poster artworks may combine these colors in different proportions, but the interface should remain recognizable across every page.

Use the locally licensed Geist family for all live interface text. The word Assembly is an oversized, tightly tracked display treatment rather than a separate decorative font asset. Headlines use heavy or semibold weights and compact line height. Body paragraphs remain around sixteen to nineteen pixels with generous leading. Primary controls and navigation are at least fourteen pixels; secondary metadata is at least twelve. Boldness must not come at the cost of readable programme details or tiny mobile controls.

The visual language is based on a square, a circle, a set of parallel lines, and the act of rearranging them. These are simple enough to become a flexible identity and distinctive enough to give the site a point of view. Use original code-authored SVG poster compositions for the fictional speakers. Do not use fake headshots or imply that a real person has agreed to appear. The artwork represents a practice, not a portrait.

Avoid a dark concert shell, neon glows, fake live-performance photographs, ticket tiers, countdown urgency, and invented sponsor logos. Assembly is a daytime creative conference. Its energy comes from large type, unexpected proportion, flat color, and the relationship between ideas and making. Keep corners mostly square, with strong two-pixel borders and simple rectangular actions. The design should feel like a printed programme translated into a useful website.

## Home page composition

The top header is a three-cell grid: Assembly, Programme, and My itinerary. The first cell is vermilion, the second yellow, and the third cobalt-violet with white text. Each is a real link. The itinerary cell contains the current local selection count. Use a height around seventy-four pixels on desktop and sixty-seven on a phone, with visible internal rules. The cells should remain readable at narrow widths rather than collapsing into an unexplained icon menu.

Below the header, place the huge ASSEMBLY word across the vermilion hero. Its desktop font size can approach seventeen percent of the viewport width, with heavy weight, very tight tracking, and a line height around 0.85. It should fit across the canvas without accidental horizontal overflow. Beneath it, use a two-column information area: the phrase “Ideas meet. Hands make.” on the left and the fictional dates and central question on the right. The dates are 20–21 May 2027, and all programme times use UTC for an unambiguous demonstration.

The lower hero combines a broad original geometric composition and an explanatory block. The geometric area has a cream background, a tilted violet square, a yellow circle, and black parallel lines. A real Shift the composition button changes the arrangement. It should move the shapes through a short, bounded transition and then stop. The button changes to Reset the composition and uses a pressed state. This is an invitation to play with the identity, not an autoplay animation.

The explanatory block says that Assembly is a fictional gathering for people who think through making. Its main action opens the programme. Do not use Get tickets or Reserve a seat because those actions do not exist. A secondary navigation row follows, linking to Speakers, Venue and access, and All sessions, alongside a concise fictional-conference label. The top of the page should establish both the visual identity and the actual actions a visitor can take.

The next section introduces three fictional voices through their original posters, disciplines, names, and a short point of view. A violet manifesto section follows with a statement about curiosity, incomplete ideas, and making. Then show three format panels for talks, workshops, and conversations. These panels use different flat colors but one consistent structure. Continue with a small programme teaser containing real session cards that can be added to the itinerary, then a venue-concept preview and a closing invitation to plan.

## Fictional speakers and poster systems

Ada Vale is a fictional identity and systems designer. Her point of view is that a good identity makes room for change. The original project, The open shape, begins with a divided square and explores how rotation, recombination, and proportion can produce related compositions. Her poster uses vermilion, cream, yellow, and charcoal, with a large Open systems headline and a geometric construction. It should feel like a visual argument about flexible identity rather than a random arrangement of shapes.

Milo Arden is a fictional typography and editorial designer. His point of view is that typography gives an idea its pace. The original project, Words in motion, uses repeated short words, large scale, strong rules, and a shifted black band to explore reading rhythm. The poster’s central phrase is Read. The. Room. It uses yellow and charcoal with cream secondary type. Its visual tension comes from direct words and the interruption of a strict grid.

Nora Sen is a fictional creative coder and visual artist. Her point of view is that a constraint can be a place to begin. The original project, A field of possibilities, uses repeated circles with small changes in size and position. A violet field and cream outlines make the pattern clear; a few yellow and vermilion interruptions make the choices visible. The work should demonstrate a readable system with deliberate exceptions, not claim that an algorithm produced meaningful art automatically.

Each speaker page needs a substantial introduction, a description of the practice, a project explanation, a broad detail of the same original poster, a practical takeaway, and all relevant sessions. Clearly label the speaker and project as fictional. Do not fabricate client lists, awards, real locations, years of professional experience, or external portfolio links. The profile should have enough depth to help someone choose a session based on the ideas, not merely the visual appeal of the poster.

## Programme data and structure

Create ten sessions across two days, with two tracks: Ideas and Practice. Ideas includes talks and one shared conversation, hosted in the Forum. Practice includes ninety-minute workshops in the Studio. The data contains a stable session identifier, day, start and end times, track, format, room, speaker identifier, title, and description. Times use zero-padded twenty-four-hour strings and are interpreted as UTC throughout the demonstration.

Day one includes Ada’s opening identity talk at 09:30–10:15, Milo’s reading-rhythm talk at 10:30–11:15, Ada’s shape workshop at 10:45–12:15, Nora’s small-rules talk at 11:30–12:15, and a shared conversation at 14:00–15:00. The workshop deliberately overlaps two talks, creating a meaningful choice for the itinerary. No speaker is scheduled in two places at the same time. The midday break remains open.

Day two includes Nora’s constraint workshop at 09:30–11:00, Milo’s typography talk at 10:00–10:45, Ada’s consistency talk at 11:15–12:00, Milo’s poster workshop at 13:00–14:30, and Nora’s human-choice talk at 14:00–14:45. These overlaps are intentional and should be visible in planning. A session that ends exactly when another begins is not considered overlapping by the interval rule, although a real attendee might choose additional travel or rest time.

The programme page has a large heading, a clear fictional-event notice, day buttons, a track select, a result count, and a chronological session list. Day and track filters combine with AND semantics. Both days and both tracks are the defaults. The session cards show the day, start and end, room, track, format, title, description, speaker link, and an Add to itinerary button. Do not compress the schedule into a wide table that becomes unreadable on a phone.

On desktop, use a three-column row with time information at the left, the session content in the center, and the planning action at the right. On mobile, stack these roles while preserving the order. The time should remain easy to find, the title should be around twenty-seven pixels, and the description should stay at sixteen. The page includes a meaningful h2 before the session h3 headings so the outline does not skip a level.

## Local itinerary model

Store only selected session identifiers under a dedicated local-storage key. Load defensively: accept an array, discard unknown identifiers, and deduplicate it. The source session data remains immutable. The selection count in the header and the pressed state of every visible session action derive from the same array. A session saved on its speaker page must appear selected when the visitor later opens the programme or itinerary.

Adding a nonconflicting session saves it immediately and announces the result. Clicking a selected session removes it. The action label and pressed state must reflect whether the session is currently included. Give each button an accessible name that includes the visible action phrase and the session title, so repeated Add buttons can be distinguished by assistive technology. If browser storage is unavailable, retain the current in-memory plan and explain that it should be exported before leaving.

Use one pure overlap function for all planning behavior. Two sessions overlap only when they occur on the same day and the first starts before the second ends and the second starts before the first ends. This strict comparison allows adjacent intervals to meet at a boundary without being flagged. Use the same function when adding a session, rendering itinerary warnings, and building the export. Do not duplicate slightly different overlap logic in separate handlers.

## Conflict-resolution interaction

If a new session overlaps one or more saved selections, open a native modal dialog. Its heading says that the sessions overlap. Show the new session with day and time, then list every conflicting saved session with its time. The visitor should understand exactly what would change before choosing an action. Do not silently replace an existing selection or refuse to add a session without explanation.

Offer three explicit choices: Keep both and flag the overlap, Replace overlapping sessions, and Cancel. Keep both adds the new session and preserves all existing choices; the itinerary then shows the conflict. Replace removes only the sessions that overlap the new one, keeps unrelated selections, and adds the new session. Cancel leaves the plan unchanged. The dialog’s default focus should be the safe Cancel action, and Escape should close it without changing the selection.

Use the native dialog element for focus containment and background inertness. On close, restore focus to the button that initiated the choice if it still exists. If the itinerary was rerendered and that button was removed, focus the itinerary summary heading instead. The dialog must fit within a 390-pixel viewport, use readable sixteen-pixel explanatory text, and have fourteen-pixel or larger controls with comfortable touch areas. Do not require hover or drag gestures.

## Itinerary page and export

The itinerary page has a large title and a practical two-column layout. The summary panel shows selected-session count and overlap-pair count, then an export action. The main column lists selected sessions chronologically, with day, time, title, speaker, format, room, and a removal action. Sessions involved in an overlap receive a visible text warning, not only a color change. A summary message explains that alternatives can be kept or removed.

An empty itinerary has a useful invitation back to the programme and a disabled export action. Removing the last session returns to that state. When a session is removed from the dynamic list, focus should move to a stable summary element so keyboard users do not lose their position. The summary updates from the actual selected data. Do not display invented ticket status, seat availability, or capacity numbers.

Prepare export creates both a Markdown plan and a tentative iCalendar file. Show the plain-language Markdown in a read-only text area, provide real download anchors with Blob URLs and descriptive filenames, and offer Copy itinerary. The Markdown includes all selected session details and an explicit overlap section. If conflicting alternatives were kept, the export must retain that warning rather than pretending the plan is conflict-free.

The calendar file contains stable event identifiers, a timestamp, UTC start and end times, session titles, fictional room labels, a local-demo description, and STATUS:TENTATIVE. It contains no attendee email addresses, organizer identity, meeting URL, or invitation-sending method. Escape calendar text values appropriately and use CRLF line endings. Long lines should be folded so the file is a well-formed portable calendar document. Importing it is the visitor’s choice and is separate from using the webpage.

Changing the selected plan invalidates previously prepared export links and text. This prevents the visitor from downloading a stale plan after removing or replacing a session. Revoke old Blob URLs before preparing replacements. Copy reports the actual Clipboard API result; if copying fails, focus and select the visible text and leave the download links available. Do not claim that a calendar import or file download completed unless that completion was actually observed.

## Venue and access guide

Assembly Works is an imagined venue, not a real address. The concept has three spaces: the Forum for talks, the Studio for workshops, and the Commons connecting them. Use a simple original diagram that shows those relationships. Label it as a conceptual map, not an accurate floor plan. It should help explain the programme’s room names without encouraging anyone to travel to a nonexistent event.

The guide discusses the intended arrival experience, readable room labels, and the role of a shared pause space. It must also distinguish design intent from verified physical access. A real event would need confirmed step-free routes, accessible facilities, seating information, captioning, sensory conditions, food information, and a named access contact. None of those can be promised for a fictional building. Explain the missing verification plainly without inventing a compliance badge.

The website’s own accessibility can be described concretely: keyboard-operable navigation, visible focus, readable controls, explicit overlap messages, and reduced-motion support. Do not equate those interface choices with physical venue accessibility. The guide should end with a useful route back to the personal itinerary and a reminder that the exported plan is not a ticket or reservation.

## Responsive design and motion

At around one thousand pixels, reduce the spacing between columns and the size of the hero’s geometric elements. At around seven hundred and twenty pixels, stack the hero’s information blocks, the speaker grid, format panels, venue preview, profile sections, and itinerary layout. Programme rows become vertical cards with clear time information at the top. Day filters wrap, and the track select becomes full width. The three-cell header remains legible.

Do not shrink actual content into a miniature desktop representation. Use at least fourteen pixels for controls and navigation, twelve for secondary metadata, and sixteen to nineteen for body reading. The oversized ASSEMBLY title is decorative display typography but must still fit without causing document overflow. Poster details can be cropped for composition, while the original full poster appears elsewhere on the profile. Captions explain when a wide image is a detail of the same artwork.

Motion is bounded and user-driven. The hero’s composition shift moves shapes once and settles. Poster images may scale slightly on hover. There are no infinite marquees, automatic carousels, flashing elements, or fake live-event animations. Respect prefers-reduced-motion by removing transitions and smooth scrolling. All states remain clear without animation.

## Build contract and verification

Export buildAssembly from scripts/assembly/build.mjs and return an object containing prompt. Generate eight complete pages: home, programme, speaker index, venue and access, itinerary, and three speaker/project pages. Keep source in scripts/assembly, generated pages and assets in public/demos/assembly, and the distributed prompt in public/prompts/assembly.md. Include this substantive brief, exact event data and pure calendar logic, responsive CSS, and actual JavaScript under Reference interaction handler.

Deliver three original SVG poster systems, an original hero SVG and raster hero.webp, a local Geist font, its license, and clear provider-neutral credits. Do not add a favicon or unrelated platform branding. Do not change shared application files, catalogs, validators, or deployment configuration. The parent task owns integration and publishing.

Run scoped lint and test the overlap function for overlapping intervals, nonoverlapping intervals, adjacent boundaries, and different days. Verify that no fictional speaker has an impossible simultaneous commitment in the source programme. In the browser, exercise day and track filters together, add a nonconflicting session, cancel a conflict, keep conflicting alternatives, replace multiple overlaps, remove a selection, restore the saved plan, and prepare the exports. Inspect the Markdown and calendar link destinations and confirm clipboard feedback.

Check desktop and mobile layouts, including the conflict dialog and itinerary controls. Measure document width at a narrow viewport and fix any overflow at its source. Report the actual outcomes and any limitations precisely. The finished site should pair the confidence of a design-festival identity with a small but genuinely useful planning tool, all within an honest fictional-conference boundary.


## Exact original event data and calendar logic

```javascript
export const speakers = [
  {
    id: 'ada-vale',
    name: 'Ada Vale',
    discipline: 'Identity & systems',
    line: 'A good identity makes room for change.',
    color: '#f05c3b',
    ink: '#20221d',
    project: 'The open shape',
    bio: 'Ada Vale is a fictional identity designer exploring how a simple visual idea can remain recognisable as it changes. Her practice in this conference concept moves between naming, typography, and adaptable graphic systems.',
    approach:
      'The work begins with a small rule rather than a finished logo. A repeated shape, a particular proportion, or a distinctive rhythm becomes the starting point for a system that can answer different needs. Consistency comes from the relationship between elements, not from making every application identical.',
    projectCopy:
      'The open shape is an original geometric identity study created for Assembly. A square is divided, turned, and recombined to make a family of related compositions. The result tests how much an identity can change before its character disappears.',
    takeaway:
      'Leave with a practical way to distinguish a useful design principle from a rigid visual template.',
  },
  {
    id: 'milo-arden',
    name: 'Milo Arden',
    discipline: 'Type & editorial',
    line: 'Typography gives an idea its pace.',
    color: '#ddd653',
    ink: '#20221d',
    project: 'Words in motion',
    bio: 'Milo Arden is a fictional editorial designer interested in the way typography directs attention. His conference sessions use ordinary words, deliberate scale, and a few simple constraints to explore how a page can feel both clear and alive.',
    approach:
      'A typographic decision is also a decision about time. A large word stops the eye; a measured paragraph keeps it moving. Milo’s concept practice treats the headline, caption, and margin as parts of the same reading experience, rather than separate styling exercises.',
    projectCopy:
      'Words in motion is an original typographic poster study made for this conference. A few repeated words change scale and position while a strict underlying grid holds the composition together. The tension between order and interruption becomes the visual idea.',
    takeaway:
      'Leave with a sharper eye for hierarchy, line length, and the small spacing decisions that make a page readable.',
  },
  {
    id: 'nora-sen',
    name: 'Nora Sen',
    discipline: 'Creative coding & form',
    line: 'A constraint can be a place to begin.',
    color: '#5148d7',
    ink: '#f8f5e9',
    project: 'A field of possibilities',
    bio: 'Nora Sen is a fictional creative coder and visual artist exploring systems that produce unexpected forms. Her work in the Assembly concept connects simple geometric instructions with questions about authorship, variation, and deliberate choice.',
    approach:
      'The interesting part of a generative system is not how many images it can make. It is the relationship between its rules and the decisions a person makes around them. Nora’s sessions focus on readable systems: small enough to understand, open enough to surprise.',
    projectCopy:
      'A field of possibilities is an original geometric poster series created for Assembly. Repeated circular forms are interrupted by a small number of changes in position and scale. A limited palette makes those changes easy to see and compare.',
    takeaway:
      'Leave with a small set of constraints you can adapt into your own visual experiments, with no coding experience required.',
  },
];
export const sessions = [
  {
    id: 'open-systems',
    day: '2027-05-20',
    start: '09:30',
    end: '10:15',
    track: 'Ideas',
    format: 'Talk',
    room: 'Forum',
    speaker: 'ada-vale',
    title: 'An identity is an open system',
    description:
      'How a simple visual principle can hold a brand together without making every expression the same.',
  },
  {
    id: 'reading-rhythm',
    day: '2027-05-20',
    start: '10:30',
    end: '11:15',
    track: 'Ideas',
    format: 'Talk',
    room: 'Forum',
    speaker: 'milo-arden',
    title: 'The rhythm of a reading experience',
    description:
      'A close look at the relationship between a headline, a paragraph, and the space around them.',
  },
  {
    id: 'shape-workshop',
    day: '2027-05-20',
    start: '10:45',
    end: '12:15',
    track: 'Practice',
    format: 'Workshop',
    room: 'Studio',
    speaker: 'ada-vale',
    title: 'One shape, many possibilities',
    description:
      'Build a flexible identity from a single geometric starting point. Paper and a pencil are enough.',
  },
  {
    id: 'small-rules',
    day: '2027-05-20',
    start: '11:30',
    end: '12:15',
    track: 'Ideas',
    format: 'Talk',
    room: 'Forum',
    speaker: 'nora-sen',
    title: 'Small rules, unexpected forms',
    description:
      'What a simple generative system can teach us about control, variation, and choosing when to stop.',
  },
  {
    id: 'unfinished',
    day: '2027-05-20',
    start: '14:00',
    end: '15:00',
    track: 'Ideas',
    format: 'Conversation',
    room: 'Forum',
    speaker: 'all',
    title: 'In praise of the unfinished',
    description:
      'All three fictional speakers discuss the role of incomplete ideas, rough work, and useful uncertainty.',
  },
  {
    id: 'constraint-lab',
    day: '2027-05-21',
    start: '09:30',
    end: '11:00',
    track: 'Practice',
    format: 'Workshop',
    room: 'Studio',
    speaker: 'nora-sen',
    title: 'The constraint lab',
    description:
      'Make a family of visual compositions from a short set of rules. No coding knowledge required.',
  },
  {
    id: 'type-personality',
    day: '2027-05-21',
    start: '10:00',
    end: '10:45',
    track: 'Ideas',
    format: 'Talk',
    room: 'Forum',
    speaker: 'milo-arden',
    title: 'Type has a point of view',
    description:
      'How typographic choices communicate character before anyone reads the words.',
  },
  {
    id: 'useful-consistency',
    day: '2027-05-21',
    start: '11:15',
    end: '12:00',
    track: 'Ideas',
    format: 'Talk',
    room: 'Forum',
    speaker: 'ada-vale',
    title: 'Useful consistency, not repetition',
    description:
      'A practical framework for deciding what stays fixed and what can move inside a visual identity.',
  },
  {
    id: 'poster-workshop',
    day: '2027-05-21',
    start: '13:00',
    end: '14:30',
    track: 'Practice',
    format: 'Workshop',
    room: 'Studio',
    speaker: 'milo-arden',
    title: 'A poster with something to say',
    description:
      'Turn a short sentence into a clear typographic composition, then test it at a different scale.',
  },
  {
    id: 'human-choice',
    day: '2027-05-21',
    start: '14:00',
    end: '14:45',
    track: 'Ideas',
    format: 'Talk',
    room: 'Forum',
    speaker: 'nora-sen',
    title: 'The human choice in a system',
    description:
      'Why editing, context, and intention matter as much as the rules that generate a form.',
  },
];
export function sessionsOverlap(a, b) {
  return a.day === b.day && a.start < b.end && b.start < a.end;
}
export function conflictPairs(selected) {
  const pairs = [];
  for (let i = 0; i < selected.length; i++)
    for (let j = i + 1; j < selected.length; j++)
      if (sessionsOverlap(selected[i], selected[j]))
        pairs.push([selected[i], selected[j]]);
  return pairs;
}
export function calendarText(selected) {
  const stamp = new Date()
    .toISOString()
    .replace(/[-:]/g, '')
    .replace(/\.\d{3}/, '');
  const escape = (value) =>
    value
      .replaceAll('\\', '\\\\')
      .replaceAll('\n', '\\n')
      .replaceAll(',', '\\,')
      .replaceAll(';', '\\;');
  const instant = (day, time) =>
    day.replaceAll('-', '') + 'T' + time.replace(':', '') + '00Z';
  const events = selected.map((session) =>
    [
      'BEGIN:VEVENT',
      `UID:assembly-${session.id}@example.invalid`,
      `DTSTAMP:${stamp}`,
      `DTSTART:${instant(session.day, session.start)}`,
      `DTEND:${instant(session.day, session.end)}`,
      `SUMMARY:${escape(session.title)}`,
      `LOCATION:${escape(session.room + ' - fictional Assembly venue')}`,
      'DESCRIPTION:Fictional conference itinerary. Not a ticket or reservation. All demonstration times use UTC.',
      'STATUS:TENTATIVE',
      'END:VEVENT',
    ].join('\r\n'),
  );
  return [
    'BEGIN:VCALENDAR',
    'VERSION:2.0',
    'PRODID:-//Assembly Concept//Local Itinerary//EN',
    'CALSCALE:GREGORIAN',
    ...events,
    'END:VCALENDAR',
    '',
  ]
    .join('\r\n')
    .split('\r\n')
    .map((line) => line.match(/.{1,73}/g)?.join('\r\n ') || '')
    .join('\r\n');
}

```

## Exact responsive CSS

```css
@font-face {
  font-family: Assembly;
  src: url('assets/geist-variable.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
}
* {
  box-sizing: border-box;
}
html {
  scroll-behavior: smooth;
  scroll-padding-top: 90px;
}
body {
  margin: 0;
  background: #f8f5e9;
  color: #20221d;
  font:
    16px/1.65 Assembly,
    Arial,
    sans-serif;
  -webkit-font-smoothing: antialiased;
}
button,
input,
select,
textarea {
  font: inherit;
}
button {
  cursor: pointer;
  color: inherit;
}
a {
  color: inherit;
  text-decoration: none;
}
h1,
h2,
h3,
p {
  margin: 0;
}
h1,
h2,
h3 {
  font-weight: 650;
  letter-spacing: -0.055em;
  line-height: 1.02;
}
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible {
  outline: 3px solid #5148d7;
  outline-offset: 4px;
}
::selection {
  background: #ddd653;
  color: #20221d;
}
.skip {
  position: fixed;
  top: -100px;
  left: 20px;
  z-index: 90;
  background: #fff;
  padding: 14px;
}
.skip:focus {
  top: 0;
}
.header {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  height: 74px;
  border-bottom: 2px solid #20221d;
}
.header a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 25px;
  font-size: 14px;
  font-weight: 550;
  border-right: 2px solid #20221d;
}
.header a:last-child {
  border-right: 0;
}
.header .brand {
  font-size: 25px;
  letter-spacing: -1px;
  background: #f05c3b;
}
.header .program-link {
  background: #ddd653;
}
.header .plan-link {
  background: #5148d7;
  color: #fff;
}
.count {
  font-size: 14px;
  border: 1px solid currentColor;
  min-width: 28px;
  height: 28px;
  display: inline-grid;
  place-items: center;
  margin-left: 10px;
}
.hero {
  background: #f05c3b;
  padding: 28px 25px 35px;
  border-bottom: 2px solid #20221d;
  overflow: hidden;
}
.hero h1 {
  font-size: clamp(95px, 17.4vw, 290px);
  line-height: 0.85;
  letter-spacing: -0.085em;
  font-weight: 760;
  margin-left: -9px;
  white-space: nowrap;
}
.hero-sub {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  gap: 40px;
  margin-top: 40px;
  align-items: end;
}
.hero-sub h2 {
  font-size: clamp(35px, 3.8vw, 59px);
  line-height: 1.02;
  max-width: 540px;
}
.hero-meta {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 25px;
  font-size: 16px;
}
.hero-meta strong {
  display: block;
  font-size: 25px;
  letter-spacing: -0.7px;
  line-height: 1.2;
  margin-top: 7px;
}
.eyebrow {
  font-size: 12px;
  font-weight: 550;
  letter-spacing: 1px;
  text-transform: uppercase;
  display: block;
  line-height: 1.6;
}
a.eyebrow {
  font-size: 14px;
}
.hero-lower {
  margin-top: 35px;
  display: grid;
  grid-template-columns: 1.5fr 1fr;
  gap: 25px;
  align-items: end;
}
.hero-art {
  height: 260px;
  background: #f8f5e9;
  border: 2px solid #20221d;
  position: relative;
  overflow: hidden;
}
.art-square {
  position: absolute;
  width: 180px;
  height: 180px;
  background: #5148d7;
  left: 12%;
  top: 40px;
  transform: rotate(-13deg);
  transition:
    transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1),
    left 0.6s;
}
.art-circle {
  position: absolute;
  width: 220px;
  height: 220px;
  border-radius: 50%;
  background: #ddd653;
  left: 43%;
  top: 15px;
  border: 2px solid #20221d;
  transition:
    transform 0.6s,
    left 0.6s;
}
.art-lines {
  position: absolute;
  width: 120px;
  height: 210px;
  right: 9%;
  top: 25px;
  background: repeating-linear-gradient(
    0deg,
    #20221d,
    #20221d 10px,
    transparent 10px,
    transparent 24px
  );
  transform: rotate(15deg);
  transition: transform 0.6s;
}
.hero-art.shifted .art-square {
  left: 28%;
  transform: rotate(30deg);
}
.hero-art.shifted .art-circle {
  left: 8%;
  transform: scale(0.78);
}
.hero-art.shifted .art-lines {
  transform: rotate(-25deg);
}
.hero-action p {
  font-size: 16px;
  line-height: 1.7;
  max-width: 420px;
  margin-bottom: 24px;
}
.button {
  display: inline-flex;
  justify-content: space-between;
  align-items: center;
  gap: 30px;
  background: #20221d;
  color: #fff;
  border: 2px solid #20221d;
  padding: 14px 20px;
  min-height: 50px;
  font-size: 14px;
  font-weight: 550;
}
.button.light {
  background: #f8f5e9;
  color: #20221d;
}
.button:hover {
  background: #5148d7;
  border-color: #5148d7;
  color: #fff;
}
.hero-action .button {
  width: 100%;
  max-width: 400px;
}
.art-control {
  font-size: 14px;
  border: 0;
  background: none;
  padding: 13px 0 0;
  text-decoration: underline;
  text-underline-offset: 4px;
}
.secondary-nav {
  display: flex;
  justify-content: space-between;
  gap: 25px;
  padding: 17px 25px;
  border-bottom: 2px solid #20221d;
  font-size: 14px;
  font-weight: 550;
}
.secondary-nav div {
  display: flex;
  gap: 30px;
}
.section {
  padding: 75px 4%;
}
.section-heading {
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  gap: 12%;
  align-items: end;
  margin-bottom: 45px;
}
.section-heading h2 {
  font-size: clamp(39px, 4.6vw, 70px);
}
.section-heading p {
  font-size: 18px;
  line-height: 1.7;
  max-width: 500px;
}
.speaker-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 25px;
}
.speaker-card {
  min-width: 0;
}
.speaker-card > a {
  display: block;
  overflow: hidden;
}
.speaker-card img {
  width: 100%;
  height: auto;
  aspect-ratio: 4/5;
  object-fit: cover;
  display: block;
  transition: transform 0.4s;
}
.speaker-card > a:hover img {
  transform: scale(1.025);
}
.speaker-info {
  padding-top: 20px;
  border-bottom: 2px solid #20221d;
  padding-bottom: 22px;
}
.speaker-info h3 {
  font-size: 31px;
  margin: 8px 0 14px;
}
.speaker-info p {
  font-size: 16px;
  line-height: 1.7;
  margin-bottom: 20px;
}
.text-link {
  display: inline-flex;
  gap: 28px;
  border-bottom: 1px solid;
  padding: 7px 0;
  font-size: 14px;
}
.manifesto {
  background: #5148d7;
  color: #f8f5e9;
  display: grid;
  grid-template-columns: 1fr 1.5fr;
  gap: 12%;
}
.manifesto h2 {
  font-size: clamp(40px, 5.2vw, 78px);
}
.manifesto p {
  font-size: 19px;
  line-height: 1.8;
  margin-bottom: 30px;
}
.format-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0;
  border: 2px solid #20221d;
}
.format-card {
  padding: 32px;
  border-right: 2px solid #20221d;
  background: #ddd653;
}
.format-card:nth-child(2) {
  background: #f05c3b;
}
.format-card:last-child {
  border: 0;
  background: #d3d0ec;
}
.format-card h3 {
  font-size: 34px;
  margin: 35px 0 20px;
}
.format-card p {
  font-size: 16px;
  line-height: 1.8;
}
.program-teaser {
  border-top: 2px solid #20221d;
}
.session-list {
  border-top: 2px solid #20221d;
}
.session {
  display: grid;
  grid-template-columns: 150px 1fr 180px;
  gap: 30px;
  padding: 27px 0;
  border-bottom: 1px solid #9b9e8c;
  align-items: start;
}
.session-time {
  font-size: 16px;
  line-height: 1.8;
}
.session-time strong {
  font-size: 20px;
  display: block;
  letter-spacing: -0.5px;
}
.session h3 {
  font-size: 27px;
  line-height: 1.15;
  margin: 7px 0 13px;
}
.session p {
  font-size: 16px;
  line-height: 1.75;
  max-width: 700px;
  margin-bottom: 12px;
}
.session-person {
  font-size: 14px;
  text-decoration: underline;
  text-underline-offset: 4px;
}
.save-button {
  border: 2px solid #20221d;
  background: #f8f5e9;
  padding: 12px 14px;
  min-height: 47px;
  font-size: 14px;
  font-weight: 550;
}
.save-button[aria-pressed='true'] {
  background: #ddd653;
}
.save-button:hover {
  background: #e8e3ca;
}
.venue-teaser {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  gap: 9%;
  background: #ddd653;
  align-items: center;
}
.venue-teaser h2 {
  font-size: clamp(40px, 5vw, 74px);
  margin: 20px 0;
}
.venue-teaser p {
  font-size: 17px;
  line-height: 1.8;
}
.venue-map {
  background: #f8f5e9;
  padding: 24px;
  border: 2px solid #20221d;
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  gap: 15px;
  min-height: 330px;
}
.map-room {
  border: 2px solid #20221d;
  padding: 22px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  font-size: 15px;
  background: #d3d0ec;
}
.map-room strong {
  font-size: 24px;
  letter-spacing: -0.7px;
}
.map-room:nth-child(2) {
  background: #f1baa7;
}
.map-room:last-child {
  grid-column: 1/-1;
  background: #ddd653;
  min-height: 95px;
}
.closing {
  padding: 65px 4%;
  background: #20221d;
  color: #f8f5e9;
  display: flex;
  justify-content: space-between;
  gap: 40px;
  align-items: center;
}
.closing h2 {
  font-size: clamp(40px, 4.8vw, 70px);
  max-width: 850px;
}
.footer {
  padding: 30px 25px;
  border-top: 2px solid #20221d;
  display: flex;
  justify-content: space-between;
  gap: 25px;
  font-size: 12px;
}
.footer nav {
  display: flex;
  gap: 25px;
  font-size: 14px;
}
.page-intro {
  padding: 55px 4% 40px;
}
.page-intro h1 {
  font-size: clamp(55px, 8vw, 120px);
  line-height: 0.96;
  margin: 20px 0 25px;
  max-width: 1150px;
}
.page-intro p {
  font-size: 18px;
  line-height: 1.8;
  max-width: 750px;
}
.page-note {
  font-size: 14px !important;
  margin-top: 20px !important;
  color: #535849;
}
.filters {
  display: flex;
  justify-content: space-between;
  gap: 25px;
  align-items: end;
  margin-bottom: 30px;
}
.day-tabs {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
.day-tabs button {
  border: 2px solid #20221d;
  background: #f8f5e9;
  padding: 12px 20px;
  font-size: 14px;
  font-weight: 550;
  min-height: 47px;
}
.day-tabs button[aria-pressed='true'] {
  background: #5148d7;
  color: #fff;
}
.track-label {
  display: grid;
  gap: 8px;
  font-size: 14px;
}
.track-label select {
  padding: 12px;
  border: 2px solid #20221d;
  background: #fff;
  font-size: 14px;
  min-height: 47px;
}
.program-count {
  font-size: 14px;
  margin: 20px 0;
}
.program {
  padding: 10px 4% 70px;
}
.status {
  font-size: 14px;
  display: block;
  line-height: 1.8;
  min-height: 25px;
  margin-top: 25px;
}
.empty {
  padding: 35px;
  background: #e9e6d6;
  border: 2px solid #20221d;
}
.empty h2,
.empty h3 {
  font-size: 32px;
  margin-bottom: 18px;
}
.empty p {
  font-size: 16px;
  line-height: 1.8;
  margin-bottom: 22px;
}
.person-head {
  display: grid;
  grid-template-columns: 1.4fr 1fr;
  gap: 8%;
  padding: 60px 4%;
  align-items: center;
}
.person-head h1 {
  font-size: clamp(65px, 8vw, 120px);
  line-height: 0.92;
  margin: 20px 0 25px;
}
.person-head p {
  font-size: 19px;
  line-height: 1.7;
  max-width: 700px;
}
.person-head img {
  width: 100%;
  height: auto;
  display: block;
  aspect-ratio: 4/5;
  object-fit: cover;
}
.person-copy {
  display: grid;
  grid-template-columns: 1fr 1.5fr;
  gap: 12%;
  padding: 65px 7%;
  border-top: 2px solid #20221d;
}
.person-copy h2 {
  font-size: 46px;
  line-height: 1.08;
  max-width: 420px;
}
.person-copy p {
  font-size: 18px;
  line-height: 1.85;
  margin-bottom: 25px;
}
.project-image {
  margin: 0;
  padding: 0 7% 65px;
}
.project-image img {
  width: 100%;
  height: auto;
  display: block;
  aspect-ratio: 1.7;
  object-fit: cover;
}
.project-image figcaption {
  font-size: 12px;
  padding-top: 12px;
}
.person-sessions {
  padding: 50px 4% 70px;
}
.person-sessions > h2 {
  font-size: 42px;
  margin-bottom: 35px;
}
.access-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 35px;
  padding: 0 4% 70px;
}
.access-card {
  padding: 30px;
  border: 2px solid #20221d;
}
.access-card h2 {
  font-size: 34px;
  margin-bottom: 22px;
}
.access-card p,
.access-card li {
  font-size: 17px;
  line-height: 1.85;
  margin-bottom: 15px;
}
.access-card ul {
  padding-left: 22px;
}
.plan-layout {
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: 45px;
  padding: 10px 4% 75px;
  align-items: start;
}
.plan-sidebar {
  padding: 25px;
  background: #ddd653;
  border: 2px solid #20221d;
  position: sticky;
  top: 25px;
}
.plan-sidebar h2 {
  font-size: 32px;
  margin-bottom: 20px;
}
.plan-sidebar p {
  font-size: 16px;
  line-height: 1.8;
  margin-bottom: 20px;
}
.plan-sidebar .button {
  width: 100%;
  padding: 13px 12px;
  font-size: 14px;
}
.button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.plan-card {
  border-top: 2px solid #20221d;
  padding: 27px 0;
}
.plan-card h2 {
  font-size: 31px;
  line-height: 1.15;
  margin: 12px 0 18px;
}
.plan-card > p {
  font-size: 16px;
  margin-bottom: 20px;
}
.overlap-label,
.overlap-summary {
  background: #f7ccb9;
  color: #5f2515;
  border: 1px solid #bb7259;
  padding: 12px 15px;
  font-size: 14px;
  line-height: 1.7;
}
.overlap-summary {
  margin-bottom: 25px;
}
.plan-card .overlap-label {
  font-size: 14px;
}
.export-panel {
  margin-top: 35px;
  padding: 25px;
  border: 2px solid #20221d;
  background: #e9e6d6;
}
.export-panel h2 {
  font-size: 31px;
  margin-bottom: 20px;
}
.export-panel textarea {
  width: 100%;
  display: block;
  min-height: 280px;
  padding: 18px;
  font:
    14px/1.8 Assembly,
    sans-serif;
  border: 1px solid #929781;
  margin: 15px 0 20px;
  resize: vertical;
}
.export-actions {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}
.export-actions .button {
  font-size: 14px;
  padding: 12px 15px;
}
.conflict-dialog {
  max-width: calc(100vw - 32px);
  width: 620px;
  padding: 32px;
  border: 2px solid #20221d;
  background: #f8f5e9;
  color: #20221d;
  box-shadow: 12px 12px 0 #20221d4d;
}
.conflict-dialog::backdrop {
  background: #20221d99;
}
.conflict-dialog h2 {
  font-size: 37px;
  margin: 15px 0 22px;
}
.conflict-dialog p,
.conflict-dialog li {
  font-size: 16px;
  line-height: 1.8;
}
.conflict-dialog ul {
  padding-left: 22px;
  margin: 20px 0;
}
.conflict-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 25px;
}
.conflict-actions button {
  font-size: 14px;
  min-height: 46px;
  padding: 11px 15px;
  border: 2px solid #20221d;
  background: #ddd653;
}
.conflict-actions button:last-child {
  background: transparent;
}
.conflict-dialog .eyebrow {
  font-size: 12px;
}
.large-word {
  font-size: clamp(75px, 14vw, 210px);
  font-weight: 750;
  letter-spacing: -0.08em;
  line-height: 0.85;
  overflow: hidden;
  white-space: nowrap;
}
[hidden] {
  display: none !important;
}
@media (max-width: 1000px) {
  .hero-sub {
    gap: 25px;
  }
  .hero-meta {
    gap: 18px;
  }
  .hero-meta strong {
    font-size: 22px;
  }
  .hero-art {
    height: 225px;
  }
  .art-square {
    width: 145px;
    height: 145px;
  }
  .art-circle {
    width: 185px;
    height: 185px;
  }
  .art-lines {
    width: 90px;
    height: 175px;
  }
  .section-heading {
    gap: 7%;
  }
  .speaker-grid {
    gap: 18px;
  }
  .speaker-info h3 {
    font-size: 27px;
  }
  .session {
    grid-template-columns: 125px 1fr 155px;
    gap: 22px;
  }
  .session h3 {
    font-size: 25px;
  }
  .plan-layout {
    grid-template-columns: 250px 1fr;
    gap: 30px;
  }
  .person-copy {
    gap: 8%;
    padding-left: 5%;
    padding-right: 5%;
  }
  .person-copy h2 {
    font-size: 39px;
  }
  .format-card {
    padding: 25px;
  }
  .format-card h3 {
    font-size: 29px;
  }
}
@media (max-width: 720px) {
  .header {
    height: 67px;
    grid-template-columns: 1fr 1fr 1fr;
  }
  .header a {
    padding: 0 13px;
    font-size: 14px;
    gap: 5px;
  }
  .header .brand {
    font-size: 21px;
  }
  .header .count {
    margin-left: 3px;
    min-width: 23px;
    height: 23px;
    font-size: 12px;
  }
  .hero {
    padding: 28px 18px;
  }
  .hero h1 {
    font-size: 17.1vw;
    margin-left: -3px;
    letter-spacing: -0.085em;
  }
  .hero-sub {
    grid-template-columns: 1fr;
    gap: 25px;
    margin-top: 30px;
  }
  .hero-sub h2 {
    font-size: 39px;
  }
  .hero-meta {
    font-size: 14px;
  }
  .hero-meta strong {
    font-size: 22px;
  }
  .hero-lower {
    grid-template-columns: 1fr;
    gap: 25px;
    margin-top: 27px;
  }
  .hero-art {
    height: 225px;
  }
  .art-square {
    left: 10%;
    width: 145px;
    height: 145px;
  }
  .art-circle {
    left: 42%;
    width: 180px;
    height: 180px;
    top: 22px;
  }
  .art-lines {
    right: 5%;
    width: 80px;
    height: 170px;
    top: 27px;
  }
  .hero-action p {
    font-size: 16px;
  }
  .hero-action .button {
    max-width: none;
  }
  .secondary-nav {
    padding: 15px 18px;
    gap: 15px;
    flex-wrap: wrap;
  }
  .secondary-nav div {
    gap: 23px;
  }
  .secondary-nav > span {
    width: 100%;
    font-size: 12px;
  }
  .section {
    padding: 50px 5%;
  }
  .section-heading {
    grid-template-columns: 1fr;
    gap: 25px;
    margin-bottom: 30px;
  }
  .section-heading h2 {
    font-size: 42px;
  }
  .section-heading p {
    font-size: 17px;
  }
  .speaker-grid {
    grid-template-columns: 1fr;
    gap: 35px;
  }
  .speaker-card img {
    aspect-ratio: 1.15;
  }
  .speaker-info h3 {
    font-size: 32px;
  }
  .manifesto {
    grid-template-columns: 1fr;
    gap: 30px;
  }
  .manifesto h2 {
    font-size: 45px;
  }
  .manifesto p {
    font-size: 17px;
  }
  .format-grid {
    grid-template-columns: 1fr;
  }
  .format-card {
    border-right: 0;
    border-bottom: 2px solid #20221d;
    padding: 25px;
  }
  .format-card h3 {
    font-size: 34px;
    margin-top: 25px;
  }
  .session {
    grid-template-columns: 1fr;
    gap: 15px;
    padding: 24px 0;
  }
  .session-time {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 15px;
    font-size: 14px;
  }
  .session-time strong {
    font-size: 18px;
  }
  .session h3 {
    font-size: 27px;
    margin-top: 8px;
  }
  .session p {
    font-size: 16px;
  }
  .save-button {
    justify-self: start;
  }
  .venue-teaser {
    grid-template-columns: 1fr;
    gap: 30px;
  }
  .venue-teaser h2 {
    font-size: 43px;
  }
  .venue-map {
    padding: 17px;
    gap: 12px;
    min-height: 300px;
  }
  .map-room {
    padding: 15px;
    font-size: 14px;
  }
  .map-room strong {
    font-size: 21px;
  }
  .closing {
    flex-direction: column;
    align-items: start;
    padding: 50px 5%;
    gap: 30px;
  }
  .closing h2 {
    font-size: 42px;
  }
  .footer {
    padding: 25px 5%;
    flex-direction: column;
    gap: 20px;
  }
  .footer nav {
    gap: 23px;
    flex-wrap: wrap;
    font-size: 14px;
  }
  .page-intro {
    padding: 35px 5% 28px;
  }
  .page-intro h1 {
    font-size: 57px;
    letter-spacing: -0.06em;
  }
  .page-intro p {
    font-size: 17px;
  }
  .page-note {
    font-size: 14px !important;
  }
  .program {
    padding: 5px 5% 55px;
  }
  .filters {
    flex-direction: column;
    align-items: stretch;
    gap: 20px;
  }
  .day-tabs {
    gap: 8px;
  }
  .day-tabs button {
    padding: 10px 12px;
    font-size: 14px;
  }
  .track-label select {
    width: 100%;
  }
  .person-head {
    grid-template-columns: 1fr;
    padding: 40px 5%;
    gap: 35px;
  }
  .person-head h1 {
    font-size: 76px;
  }
  .person-head p {
    font-size: 18px;
  }
  .person-head img {
    aspect-ratio: 1.15;
  }
  .person-copy {
    grid-template-columns: 1fr;
    gap: 27px;
    padding: 45px 5%;
  }
  .person-copy h2 {
    font-size: 39px;
  }
  .person-copy p {
    font-size: 17px;
  }
  .project-image {
    padding: 0 5% 45px;
  }
  .project-image img {
    aspect-ratio: 1.2;
  }
  .person-sessions {
    padding: 40px 5% 55px;
  }
  .person-sessions > h2 {
    font-size: 37px;
  }
  .access-grid {
    grid-template-columns: 1fr;
    padding: 0 5% 50px;
    gap: 22px;
  }
  .access-card {
    padding: 23px;
  }
  .access-card h2 {
    font-size: 31px;
  }
  .access-card p,
  .access-card li {
    font-size: 16px;
  }
  .plan-layout {
    grid-template-columns: 1fr;
    padding: 5px 5% 55px;
    gap: 30px;
  }
  .plan-sidebar {
    position: static;
    padding: 23px;
  }
  .plan-sidebar h2 {
    font-size: 32px;
  }
  .plan-card h2 {
    font-size: 29px;
  }
  .export-panel {
    padding: 22px;
  }
  .conflict-dialog {
    padding: 24px;
    box-shadow: 7px 7px 0 #20221d4d;
  }
  .conflict-dialog h2 {
    font-size: 32px;
  }
  .conflict-actions {
    gap: 10px;
  }
  .conflict-actions button {
    width: 100%;
    text-align: left;
  }
  .large-word {
    font-size: 15.6vw;
  }
}
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *:before,
  *:after {
    animation: none !important;
    transition: none !important;
  }
}
@media (max-width: 720px) {
  .person-head img {
    aspect-ratio: 5/6;
    object-fit: contain;
  }
}

```

## Reference interaction handler

```javascript
function sessionsOverlap(a, b) {
  return a.day === b.day && a.start < b.end && b.start < a.end;
}
function conflictPairs(selected) {
  const pairs = [];
  for (let i = 0; i < selected.length; i++)
    for (let j = i + 1; j < selected.length; j++)
      if (sessionsOverlap(selected[i], selected[j]))
        pairs.push([selected[i], selected[j]]);
  return pairs;
}
function calendarText(selected) {
  const stamp = new Date()
    .toISOString()
    .replace(/[-:]/g, '')
    .replace(/\.\d{3}/, '');
  const escape = (value) =>
    value
      .replaceAll('\\', '\\\\')
      .replaceAll('\n', '\\n')
      .replaceAll(',', '\\,')
      .replaceAll(';', '\\;');
  const instant = (day, time) =>
    day.replaceAll('-', '') + 'T' + time.replace(':', '') + '00Z';
  const events = selected.map((session) =>
    [
      'BEGIN:VEVENT',
      `UID:assembly-${session.id}@example.invalid`,
      `DTSTAMP:${stamp}`,
      `DTSTART:${instant(session.day, session.start)}`,
      `DTEND:${instant(session.day, session.end)}`,
      `SUMMARY:${escape(session.title)}`,
      `LOCATION:${escape(session.room + ' - fictional Assembly venue')}`,
      'DESCRIPTION:Fictional conference itinerary. Not a ticket or reservation. All demonstration times use UTC.',
      'STATUS:TENTATIVE',
      'END:VEVENT',
    ].join('\r\n'),
  );
  return [
    'BEGIN:VCALENDAR',
    'VERSION:2.0',
    'PRODID:-//Assembly Concept//Local Itinerary//EN',
    'CALSCALE:GREGORIAN',
    ...events,
    'END:VCALENDAR',
    '',
  ]
    .join('\r\n')
    .split('\r\n')
    .map((line) => line.match(/.{1,73}/g)?.join('\r\n ') || '')
    .join('\r\n');
}
const data = JSON.parse(document.getElementById('assembly-data').textContent);
const sessionMap = new Map(
  data.sessions.map((session) => [session.id, session]),
);
const speakerMap = new Map(
  data.speakers.map((speaker) => [speaker.id, speaker]),
);
const key = 'assembly-itinerary-v1';
function loadPlan() {
  try {
    const value = JSON.parse(localStorage.getItem(key) || '[]');
    return Array.isArray(value)
      ? [...new Set(value.filter((id) => sessionMap.has(id)))]
      : [];
  } catch {
    return [];
  }
}
let selected = loadPlan();
let pending = null;
let trigger = null;
let icsUrl = '';
let markdownUrl = '';
function say(text) {
  document
    .querySelectorAll('[data-status]')
    .forEach((el) => (el.textContent = text));
}
function speakerName(session) {
  return session.speaker === 'all'
    ? 'All speakers'
    : speakerMap.get(session.speaker).name;
}
function dateLabel(day) {
  return day === '2027-05-20' ? 'Thursday 20 May' : 'Friday 21 May';
}
function sortedPlan() {
  return selected
    .map((id) => sessionMap.get(id))
    .sort((a, b) => (a.day + a.start).localeCompare(b.day + b.start));
}
function invalidateExport() {
  const panel = document.querySelector('#export-panel');
  if (panel) panel.hidden = true;
  if (icsUrl) URL.revokeObjectURL(icsUrl);
  if (markdownUrl) URL.revokeObjectURL(markdownUrl);
  icsUrl = '';
  markdownUrl = '';
  for (const link of document.querySelectorAll(
    '#calendar-download, #markdown-download',
  )) {
    link.removeAttribute('href');
  }
  const text = document.querySelector('#export-text');
  if (text) text.value = '';
}
function savePlan(message) {
  invalidateExport();
  try {
    localStorage.setItem(key, JSON.stringify(selected));
    say(message);
  } catch {
    say(
      'Changes are available on this page, but browser storage is unavailable. Export before leaving.',
    );
  }
  refreshButtons();
  renderPlan();
}
function refreshButtons() {
  document
    .querySelectorAll('[data-plan-count]')
    .forEach((el) => (el.textContent = String(selected.length)));
  document.querySelectorAll('[data-session-toggle]').forEach((button) => {
    const added = selected.includes(button.dataset.sessionToggle);
    button.setAttribute('aria-pressed', String(added));
    button.textContent = added ? 'In your itinerary −' : 'Add to itinerary +';
    button.setAttribute(
      'aria-label',
      `${added ? 'In your itinerary' : 'Add to itinerary'}: ${sessionMap.get(button.dataset.sessionToggle).title}`,
    );
  });
  document
    .querySelector('#prepare-export')
    ?.toggleAttribute('disabled', selected.length === 0);
}
const conflictDialog = document.querySelector('#conflict-dialog');
function requestToggle(id, button) {
  if (selected.includes(id)) {
    selected = selected.filter((value) => value !== id);
    savePlan('Session removed from your itinerary.');
    if (document.querySelector('#plan-list'))
      document.querySelector('#plan-heading').focus();
    return;
  }
  const session = sessionMap.get(id);
  const overlaps = sortedPlan().filter((item) =>
    sessionsOverlap(item, session),
  );
  if (!overlaps.length) {
    selected.push(id);
    savePlan('Session added to your itinerary.');
    return;
  }
  pending = { id, overlaps: overlaps.map((item) => item.id) };
  trigger = button;
  document.querySelector('#conflict-title').textContent =
    'These sessions overlap.';
  document.querySelector('#conflict-new').textContent =
    `${session.title} · ${dateLabel(session.day)}, ${session.start}–${session.end} UTC`;
  const list = document.querySelector('#conflict-list');
  list.replaceChildren();
  for (const item of overlaps) {
    const li = document.createElement('li');
    li.textContent = `${item.title} · ${item.start}–${item.end} UTC`;
    list.append(li);
  }
  conflictDialog.showModal();
}
document.addEventListener('click', (event) => {
  const button = event.target.closest('[data-session-toggle]');
  if (button) requestToggle(button.dataset.sessionToggle, button);
});
document.querySelector('#keep-both')?.addEventListener('click', () => {
  if (!pending) return;
  selected.push(pending.id);
  savePlan('Both sessions kept. The overlap is flagged in your itinerary.');
  conflictDialog.close();
});
document.querySelector('#replace-overlap')?.addEventListener('click', () => {
  if (!pending) return;
  selected = selected.filter((id) => !pending.overlaps.includes(id));
  selected.push(pending.id);
  savePlan('Overlapping sessions replaced with your new choice.');
  conflictDialog.close();
});
document
  .querySelector('#cancel-conflict')
  ?.addEventListener('click', () => conflictDialog.close());
conflictDialog?.addEventListener('close', () => {
  pending = null;
  if (trigger?.isConnected) trigger.focus();
  else document.querySelector('#plan-heading')?.focus();
});
const dayButtons = document.querySelectorAll('[data-day-filter]');
let activeDay = 'all';
function filterProgram() {
  const track = document.querySelector('#track-filter').value;
  let count = 0;
  document.querySelectorAll('[data-program-session]').forEach((article) => {
    const session = sessionMap.get(article.dataset.programSession);
    article.hidden =
      (activeDay !== 'all' && session.day !== activeDay) ||
      (track !== 'all' && session.track !== track);
    if (!article.hidden) count++;
  });
  document.querySelector('#program-count').textContent =
    `${count} ${count === 1 ? 'session' : 'sessions'} shown`;
  document.querySelector('#empty-program').hidden = count > 0;
}
dayButtons.forEach((button) =>
  button.addEventListener('click', () => {
    activeDay = button.dataset.dayFilter;
    dayButtons.forEach((item) =>
      item.setAttribute('aria-pressed', String(item === button)),
    );
    filterProgram();
  }),
);
document
  .querySelector('#track-filter')
  ?.addEventListener('change', filterProgram);
if (dayButtons.length) filterProgram();
function make(tag, className, text) {
  const el = document.createElement(tag);
  if (className) el.className = className;
  if (text !== undefined) el.textContent = text;
  return el;
}
function renderPlan() {
  const list = document.querySelector('#plan-list');
  if (!list) return;
  const plan = sortedPlan();
  const conflicts = conflictPairs(plan);
  list.replaceChildren();
  document.querySelector('#empty-plan').hidden = plan.length > 0;
  document.querySelector('#plan-summary').textContent =
    `${plan.length} ${plan.length === 1 ? 'session' : 'sessions'} selected · ${conflicts.length} ${conflicts.length === 1 ? 'overlap' : 'overlaps'}`;
  const warning = document.querySelector('#overlap-summary');
  warning.hidden = conflicts.length === 0;
  warning.textContent = conflicts.length
    ? `${conflicts.length} overlapping ${conflicts.length === 1 ? 'pair needs' : 'pairs need'} your attention. Keep both as alternatives or remove a session below.`
    : '';
  for (const session of plan) {
    const card = make('article', 'plan-card');
    const collision = conflicts.some((pair) =>
      pair.some((item) => item.id === session.id),
    );
    card.append(
      make(
        'span',
        'eyebrow',
        `${dateLabel(session.day)} / ${session.start}–${session.end} UTC`,
      ),
      make('h2', '', session.title),
      make(
        'p',
        '',
        `${speakerName(session)} · ${session.format} · ${session.room}`,
      ),
    );
    if (collision)
      card.append(
        make('p', 'overlap-label', 'Overlaps with another selected session'),
      );
    const button = make('button', 'save-button', 'In your itinerary −');
    button.type = 'button';
    button.dataset.sessionToggle = session.id;
    button.setAttribute('aria-pressed', 'true');
    button.setAttribute('aria-label', 'In your itinerary: ' + session.title);
    card.append(button);
    list.append(card);
  }
}
function planMarkdown() {
  const plan = sortedPlan();
  const conflicts = conflictPairs(plan);
  return (
    '# Assembly — personal itinerary\n\nFictional design conference. Not a ticket, booking, or reservation. All demonstration times use UTC.\n\n' +
    plan
      .map(
        (session) =>
          `## ${dateLabel(session.day)} / ${session.start}–${session.end} UTC\n\n${session.title}\n${speakerName(session)} · ${session.format} · ${session.room}\n\n${session.description}\n`,
      )
      .join('\n') +
    '\n## Overlap check\n\n' +
    (conflicts.length
      ? conflicts
          .map(
            ([a, b]) =>
              `- ${a.title} overlaps with ${b.title} on ${dateLabel(a.day)}.`,
          )
          .join('\n')
      : 'No selected sessions overlap.') +
    '\n'
  );
}
document.querySelector('#prepare-export')?.addEventListener('click', () => {
  if (!selected.length) return;
  invalidateExport();
  const markdown = planMarkdown();
  icsUrl = URL.createObjectURL(
    new Blob([calendarText(sortedPlan())], {
      type: 'text/calendar;charset=utf-8',
    }),
  );
  markdownUrl = URL.createObjectURL(
    new Blob([markdown], { type: 'text/markdown;charset=utf-8' }),
  );
  document.querySelector('#calendar-download').href = icsUrl;
  document.querySelector('#markdown-download').href = markdownUrl;
  document.querySelector('#export-text').value = markdown;
  const panel = document.querySelector('#export-panel');
  panel.hidden = false;
  panel.focus();
  say('Itinerary files prepared. Review the overlap check before using them.');
});
document
  .querySelector('#copy-itinerary')
  ?.addEventListener('click', async () => {
    try {
      await navigator.clipboard.writeText(
        document.querySelector('#export-text').value,
      );
      say('Itinerary copied.');
    } catch {
      const text = document.querySelector('#export-text');
      text.focus();
      text.select();
      say('Clipboard unavailable. Copy the selected text or download a file.');
    }
  });
document.querySelector('#shift-art')?.addEventListener('click', (event) => {
  const button = event.currentTarget;
  const shifted = button.getAttribute('aria-pressed') !== 'true';
  button.setAttribute('aria-pressed', String(shifted));
  document.querySelector('.hero-art').classList.toggle('shifted', shifted);
  button.textContent = shifted
    ? 'Reset the composition ↺'
    : 'Shift the composition ↗';
});
refreshButtons();
renderPlan();

```
