/* ============================================================
   Bret Killoran — filmography
   Navy & Ice palette, filled-tint buttons. Colour stays with the posters.
   ============================================================ */

:root {
  /* Navy & Ice — deep blue-black ground, cool off-white type. */
  --bg:            #0a0f16;
  --bg-elevated:   #141c26;
  --text:          #dee7f2;
  --text-dim:      #b9c6d6;
  --text-faint:    #8793a5;
  --line:          #1a222c;
  --line-strong:   #2c3846;

  /* the one saturated colour in the UI, reserved for "this is selected" --
     everything else stays in the neutral navy/ice range so this still
     reads as deliberate rather than decorative */
  --accent:      #4d8dff;
  --accent-text: #f3f8ff;
  /* the light-blue outline every unselected control shares -- festival
     chips, the sort toggle, the genre field -- so the whole bar reads as
     one consistent system rather than a mix of bordered and borderless
     pieces */
  --outline:     color-mix(in srgb, var(--accent) 45%, transparent);

  /* filled-tint buttons: a fill instead of an outline */
  --chip-fill:       #141c26;
  --chip-fill-hover: #1d2836;

  /* hairline that frames each poster so mixed sources read as one set */
  --poster-frame:       rgba(222, 231, 242, 0.09);
  --poster-frame-hover: rgba(222, 231, 242, 0.20);

  --radius-card:   10px;
  --radius-chip:   6px;

  --gap:           clamp(12px, 1.4vw, 22px);
  --pad-x:         clamp(20px, 5vw, 72px);

  --ease:          cubic-bezier(0.22, 0.61, 0.36, 1);
  --dur-move:      420ms;
  --dur-fade:      180ms;

  --font: "Helvetica Neue", Helvetica, -apple-system, BlinkMacSystemFont,
          "Segoe UI", Arial, sans-serif;
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  padding: 0 var(--pad-x) 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ---------------------------------------------------------- masthead */

.masthead {
  padding: clamp(48px, 9vh, 104px) 0 clamp(28px, 4vh, 44px);
  text-align: center;
}

.name {
  margin: 0;
  font-size: clamp(30px, 5.2vw, 60px);
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.05;
}

.role {
  margin: clamp(10px, 1.4vh, 16px) 0 0;
  font-size: clamp(10px, 1.15vw, 13px);
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.role .amp {
  font-style: italic;
  letter-spacing: 0.1em;
  opacity: 0.7;
}

.site-nav {
  margin: clamp(18px, 2.6vh, 30px) 0 0;
  display: flex;
  justify-content: center;
  gap: clamp(18px, 2vw, 28px);
}

.site-nav-link {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-dim);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  padding-bottom: 4px;
  transition: color var(--dur-fade) var(--ease),
              border-color var(--dur-fade) var(--ease);
}

.site-nav-link:hover { color: var(--text); border-color: var(--line-strong); }

.site-nav-link.is-active {
  color: var(--accent-text);
  border-color: var(--accent);
}

/* ---------------------------------------------------------- reel */

/* A slow, continuously drifting strip of the top 25 posters. Gives strong
   credits visibility regardless of how far down the grid they sit. Breaks
   out of the body padding to run edge to edge. */
.reel {
  position: relative;
  overflow: hidden;
  margin: 0 calc(-1 * var(--pad-x)) clamp(24px, 3.5vh, 40px);
  padding: 4px 0;
  /* posters fade out at both edges rather than hard-clipping */
  -webkit-mask-image: linear-gradient(
    to right, transparent 0%, #000 7%, #000 93%, transparent 100%);
  mask-image: linear-gradient(
    to right, transparent 0%, #000 7%, #000 93%, transparent 100%);
}

.reel[hidden] { display: none; }

.reel-track {
  display: flex;
  gap: 10px;
  width: max-content;
  will-change: transform;
  animation: reel-drift var(--reel-duration, 90s) linear infinite;
  opacity: 1;
  transition: opacity var(--dur-fade) var(--ease);
}

/* Set by syncReel() in app.js while it swaps in a new filtered selection --
   fades the strip out, replaces its contents, fades back in. Same duration
   the grid's own filter transition uses (--dur-fade), so the two read as
   one system rather than two unrelated pieces of UI. */
.reel-track.is-swapping { opacity: 0; }

/* Animating an explicit set width (rather than -50%) keeps the loop seamless
   with any number of repeated sets, not just two. */
@keyframes reel-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(calc(-1 * var(--reel-set-width, 0px)), 0, 0); }
}

.reel:hover .reel-track,
.reel:focus-within .reel-track { animation-play-state: paused; }

.reel-item {
  flex: 0 0 auto;
  display: block;
  height: clamp(120px, 15vw, 200px);
  aspect-ratio: 2 / 3;
  border-radius: 6px;
  overflow: hidden;
  background: var(--bg-elevated);
  box-shadow:
    0 0 0 1px var(--poster-frame),
    0 2px 12px rgba(0, 0, 0, 0.45);
  transition:
    transform 300ms var(--ease),
    box-shadow 300ms var(--ease);
}

.reel-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

@media (hover: hover) {
  .reel-item:hover {
    transform: translateY(-4px) scale(1.03);
    box-shadow:
      0 0 0 1px var(--poster-frame-hover),
      0 10px 26px rgba(0, 0, 0, 0.6);
  }
}

.reel-item:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 3px;
}

/* No drift for anyone who asked for less motion — becomes a plain
   swipeable strip instead. app.js also renders a single set in that case. */
@media (prefers-reduced-motion: reduce) {
  .reel-track { animation: none; }
  .reel {
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  .reel::-webkit-scrollbar { display: none; }
}

/* ---------------------------------------------------------- filters */

.filters {
  max-width: 1180px;
  margin: 0 auto clamp(28px, 4vh, 48px);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  padding: 20px 0 16px;
}

.filter-group {
  padding: 7px 0;
}

.filter-group + .filter-group { margin-top: 9px; }

/* The "Genres"/"Festivals" headings are hidden so the chips can run the
   full width. Still announced by screen readers via aria-labelledby. */
.filter-label {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Justified rows: each chip keeps its own natural width -- only the SPACE
   between chips stretches, so every row still runs flush edge to edge. The
   base 6px gap is real CSS (so the browser's own line-wrapping already
   respects it when deciding how many chips fit per row); justifyChips() in
   app.js then tops that up per row with however much is left over, via
   margin-right on top of the gap. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.chips .chip { flex: 0 0 auto; }

.chip {
  appearance: none;
  border: 1px solid transparent;
  background: var(--chip-fill);
  color: var(--text-dim);
  font: inherit;
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 0.06em;
  padding: 7px 12px;
  border-radius: var(--radius-chip);
  cursor: pointer;
  white-space: nowrap;
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  transition:
    background var(--dur-fade) var(--ease),
    color var(--dur-fade) var(--ease),
    border-color var(--dur-fade) var(--ease),
    opacity var(--dur-fade) var(--ease);
}

.chip:hover:not(.is-active):not(:disabled) {
  background: var(--chip-fill-hover);
  color: var(--text);
}

.chip.is-active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
  font-weight: 500;
}

/* a chip that can no longer narrow anything down */
.chip.is-dead:not(.is-active) {
  opacity: 0.28;
  cursor: default;
}

/* Festival buttons get a thin, light-blue outline while unselected --
   otherwise their edges disappear entirely against the near-black
   background (border was transparent). Genre chips don't need this: they
   only ever render while active, so there's no unselected state to
   outline. */
#festival-chips .chip:not(.is-active):not(.chip--any) {
  border-color: var(--outline);
}

/* "Any Festival" stays dashed -- still reads as the odd one out -- but
   shares the same outline colour as every other unselected control. */
.chip.chip--any {
  background: transparent;
  border: 1px dashed var(--outline);
}

.chip.chip--any:hover:not(.is-active):not(:disabled) {
  background: transparent;
  border-color: var(--accent);
}

/* Selected, "Any Festival" fills solid blue exactly like every other
   selected chip -- three classes here so it beats both `.chip.chip--any`
   and `.chip.is-active` above, which are equal specificity to each other
   and were letting the dashed/transparent rule win on order alone. */
.chip.chip--any.is-active {
  background: var(--accent);
  border-style: solid;
  border-color: var(--accent);
  color: var(--accent-text);
}

.chip:focus-visible,
.clear:focus-visible,
.card:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 3px;
}

/* ---------------------------------------------------------- genre input */

/* Genres are typed instead of picked from a wall of buttons -- there are
   too many of them for that to stay tidy. As you type, the highest-count
   genre matching what you've typed so far is filled in and highlighted as
   a native text selection (same trick a browser address bar uses) -- no
   overlay element, just the one real input, so there's nothing that can
   sit between a click/tap and the field itself. Enter commits it as a
   removable chip to its right, and the chip set drives `activeGenres`
   exactly like before. See `autocomplete()` in app.js. */
.genre-field {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

/* Active-genre chips run to the right of the field, filling the empty
   horizontal space next to it -- only dropping to a new row (the .chips
   container itself already wraps its children) once the row actually
   runs out of width, rather than always claiming a full-width row of
   their own. */
.genre-field .chips {
  flex: 1 1 auto;
  min-width: 0;
}

.genre-input {
  flex: 0 1 240px;
  min-width: 170px;
  appearance: none;
  background: var(--chip-fill);
  border: 1px solid var(--outline);
  border-radius: var(--radius-chip);
  color: var(--text);
  caret-color: var(--text);
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.06em;
  padding: 7px 12px;
  transition:
    background var(--dur-fade) var(--ease),
    border-color var(--dur-fade) var(--ease);
}

.genre-input::placeholder { color: var(--text-faint); }

.genre-input:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--chip-fill-hover);
}

/* the auto-completed remainder is a real selection, styled dim so it reads
   as a suggestion rather than "text about to be overwritten" */
.genre-input::selection {
  background: color-mix(in srgb, var(--accent) 35%, var(--chip-fill));
  color: var(--text);
}

/* Active genre chips -- always in the "on" state, since they only exist
   while selected; clicking one removes it. */
.chip.chip--genre {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.chip.chip--genre .chip-x {
  font-size: 13px;
  line-height: 1;
  opacity: 0.55;
}

.chip.chip--genre:hover .chip-x { opacity: 1; }

/* On phones the festival row becomes a single swipeable line so it doesn't
   push the posters below the fold. Genres still wrap — the whole point is
   that all 25 are visible without hunting. */
@media (max-width: 760px) {
  .filter-group { padding: 6px 0; }

  .chips {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 2px;
    /* fade the trailing edge so it reads as scrollable */
    -webkit-mask-image: linear-gradient(
      to right, #000 calc(100% - 30px), transparent 100%);
    mask-image: linear-gradient(
      to right, #000 calc(100% - 30px), transparent 100%);
  }

  .chips::-webkit-scrollbar { display: none; }

  #festival-chips .chip { flex: 0 0 auto; max-width: none; }

  /* genres wrap instead of scrolling, so none are hidden off-screen */
  #genre-chips {
    flex-wrap: wrap;
    overflow-x: visible;
    -webkit-mask-image: none;
    mask-image: none;
  }
}

.filter-bar-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px 16px;
  margin-top: 14px;
  padding-top: 13px;
  border-top: 1px solid var(--line);
}

.foot-right {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* Segmented sort control. Two states only - the hand-set order, and newest
   first. There is deliberately no oldest-first. */
.sort {
  display: flex;
  gap: 2px;
  padding: 2px;
  background: var(--chip-fill);
  border-radius: var(--radius-chip);
}

.sort-btn {
  appearance: none;
  border: 1px solid var(--outline);
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-weight: 500;
  font-size: 10.5px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  padding: 5px 12px;
  border-radius: 4px;
  cursor: pointer;
  transition: background var(--dur-fade) var(--ease),
              color var(--dur-fade) var(--ease),
              border-color var(--dur-fade) var(--ease);
}

.sort-btn:hover:not(.is-on) { color: var(--text); }

.sort-btn.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
  font-weight: 500;
}

.sort-btn:focus-visible {
  outline: 2px solid var(--text);
  outline-offset: 2px;
}

.sort[aria-disabled="true"] { opacity: 0.3; pointer-events: none; }

.count {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

.clear {
  appearance: none;
  background: none;
  border: 0;
  font: inherit;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text);
  cursor: pointer;
  padding: 4px 0;
  border-bottom: 1px solid var(--text);
  transition: opacity var(--dur-fade) var(--ease);
}

.clear:disabled {
  opacity: 0.22;
  cursor: default;
  border-bottom-color: transparent;
}

/* ---------------------------------------------------------- grid */

.grid {
  max-width: 1180px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--gap);
}

@media (max-width: 1080px) { .grid { grid-template-columns: repeat(4, 1fr); } }
@media (max-width: 820px)  { .grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 540px)  { .grid { grid-template-columns: repeat(2, 1fr); } }

.card {
  position: relative;
  display: block;
  aspect-ratio: 2 / 3;
  border-radius: var(--radius-card);
  overflow: hidden;
  background: var(--bg-elevated);
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  will-change: transform;
  /* the frame that makes mixed-source posters read as one set */
  box-shadow:
    0 0 0 1px var(--poster-frame),
    0 2px 14px rgba(0, 0, 0, 0.45);
  transition:
    box-shadow 260ms var(--ease),
    opacity var(--dur-fade) var(--ease);
}

.card.is-hidden { display: none; }

/* used only during the FLIP transition */
.card.is-leaving {
  opacity: 0;
  transform: scale(0.94);
  pointer-events: none;
}

.card.is-entering { opacity: 0; }

.card img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 620ms var(--ease), filter 260ms var(--ease);
}

/* fallback tile when a poster file is missing */
.card .fallback {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 16px;
  text-align: center;
  font-size: 13px;
  letter-spacing: 0.06em;
  color: var(--text-dim);
  background:
    linear-gradient(160deg, #1b2634 0%, #0d131b 100%);
}

.card .meta {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 16px 15px 15px;
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.93) 0%,
    rgba(0, 0, 0, 0.72) 34%,
    rgba(0, 0, 0, 0.15) 68%,
    rgba(0, 0, 0, 0) 100%
  );
  opacity: 0;
  transition: opacity 260ms var(--ease);
}

.card .m-dir {
  font-size: clamp(12px, 1.1vw, 15px);
  font-weight: 500;
  line-height: 1.25;
  color: #fff;
  transform: translateY(7px);
  transition: transform 340ms var(--ease);
}

.card .m-title {
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.62);
  margin-top: 6px;
  transform: translateY(7px);
  transition: transform 340ms var(--ease) 30ms;
}

.card .m-role {
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.4);
  margin-top: 4px;
  transform: translateY(7px);
  transition: transform 340ms var(--ease) 55ms;
}

@media (hover: hover) {
  .card:hover {
    box-shadow:
      0 0 0 1px var(--poster-frame-hover),
      0 10px 34px rgba(0, 0, 0, 0.62);
  }
  .card:hover img { transform: scale(1.045); }
  .card:hover .meta { opacity: 1; }
  .card:hover .m-dir,
  .card:hover .m-title,
  .card:hover .m-role { transform: translateY(0); }
}

/* Touch devices get no hover, so the director sits permanently at the foot
   of the card — kept deliberately shallow so the artwork still reads. */
@media (hover: none) {
  .card .meta {
    opacity: 1;
    padding: 10px 10px 9px;
    background: linear-gradient(
      to top,
      rgba(0, 0, 0, 0.88) 0%,
      rgba(0, 0, 0, 0.55) 45%,
      rgba(0, 0, 0, 0) 100%
    );
    height: 42%;
    top: auto;
  }

  .card .m-dir,
  .card .m-title,
  .card .m-role { transform: none; }

  .card .m-dir { font-size: 11px; line-height: 1.2; }
  .card .m-title { font-size: 8.5px; margin-top: 3px; letter-spacing: 0.1em; }
  .card .m-role { display: none; }
}

.card:focus-visible .meta { opacity: 1; }
.card:focus-visible .m-dir,
.card:focus-visible .m-title,
.card:focus-visible .m-role { transform: translateY(0); }

.empty {
  max-width: 1180px;
  margin: 60px auto;
  text-align: center;
  color: var(--text-faint);
  font-size: 13px;
  letter-spacing: 0.1em;
}

/* ---------------------------------------------------------- contact page */

.contact-main {
  max-width: 640px;
  margin: 0 auto;
  padding: clamp(24px, 6vh, 64px) 0 clamp(64px, 10vh, 120px);
  text-align: center;
}

.contact-lede {
  margin: 0 0 clamp(20px, 3vh, 32px);
  color: var(--text-dim);
  font-size: clamp(14px, 1.4vw, 17px);
}

.contact-email {
  display: inline-block;
  font-size: clamp(20px, 2.6vw, 30px);
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--text);
  text-decoration: none;
  border-bottom: 1px solid var(--line-strong);
  padding-bottom: 6px;
  transition: color var(--dur-fade) var(--ease),
              border-color var(--dur-fade) var(--ease);
}

.contact-email:hover { color: var(--accent-text); border-color: var(--accent); }

.contact-divider {
  margin: clamp(32px, 5vh, 56px) 0;
  border: 0;
  border-top: 1px solid var(--line);
}

.contact-form {
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* honeypot -- hidden from sighted and AT users, left open for bots */
.form-hp {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.form-field { display: flex; flex-direction: column; gap: 7px; }

.form-label {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.form-input,
.form-textarea {
  appearance: none;
  width: 100%;
  background: var(--chip-fill);
  border: 1px solid var(--outline);
  border-radius: var(--radius-chip);
  color: var(--text);
  caret-color: var(--text);
  font: inherit;
  font-size: 14px;
  padding: 10px 12px;
  transition: background var(--dur-fade) var(--ease),
              border-color var(--dur-fade) var(--ease);
}

.form-textarea { resize: vertical; min-height: 140px; line-height: 1.5; }

.form-input::placeholder,
.form-textarea::placeholder { color: var(--text-faint); }

.form-input:focus,
.form-textarea:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--chip-fill-hover);
}

.form-submit {
  align-self: flex-start;
  appearance: none;
  border: 1px solid var(--accent);
  background: var(--accent);
  color: var(--accent-text);
  font: inherit;
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 11px 22px;
  border-radius: var(--radius-chip);
  cursor: pointer;
  transition: opacity var(--dur-fade) var(--ease);
}

.form-submit:hover { opacity: 0.85; }

.form-submit:disabled { opacity: 0.4; cursor: default; }

.form-status {
  font-size: 12px;
  letter-spacing: 0.04em;
  min-height: 1.2em;
}

.form-status.is-error { color: #ff8a8a; }
.form-status.is-ok { color: var(--text-dim); }

/* ---------------------------------------------------------- footer */

.site-foot {
  max-width: 1180px;
  margin: 0 auto;
  padding: 64px 0 72px;
  text-align: center;
}

.site-foot a {
  color: var(--text-dim);
  text-decoration: none;
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--line-strong);
  padding-bottom: 3px;
  transition: color var(--dur-fade) var(--ease),
              border-color var(--dur-fade) var(--ease);
}

.site-foot a:hover { color: var(--text); border-color: var(--text-dim); }

/* ---------------------------------------------------------- a11y */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
