/* vivekkoshy.com
   Identity is structure and type. There is no brand colour anywhere: links,
   hover and active states are carried by weight, rule and position. The
   photographs supply every scrap of colour on the site.
   See docs/design-decisions.md for why. */

/* ---------------------------------------------------------------- fonts  */
/* Self-hosted and subset. No CDN request. If the files are absent the
   fallback stacks below still produce a correct, if plainer, site. */

@font-face {
  font-family: "Fraunces";
  src: url("/assets/fonts/Fraunces-af7778e69c28.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
}
@font-face {
  font-family: "Plex Mono";
  src: url("/assets/fonts/IBMPlexMono-Regular-84aaa05a60c5.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "Plex Mono";
  src: url("/assets/fonts/IBMPlexMono-Medium-b12255c75795.woff2") format("woff2");
  font-weight: 500;
  font-display: swap;
}

/* ---------------------------------------------------------------- tokens */

:root {
  color-scheme: light;

  /* A neutral ramp. Deliberately not warm: cream is the thing being left. */
  --paper:       #fbfbfb;
  --ink:         #141414;
  --ink-muted:   #5a5a5a;
  --line:        #dedede;
  --line-strong: #b0b0b0;
  --shade:       #efefef;

  --work:   "Fraunces", Charter, Cambria, Georgia, serif;
  --record: "Plex Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;

  /* No display type. The largest thing on the site is about 2rem. */
  --t-record: 0.75rem;
  --t-small:  0.875rem;
  --t-body:   1.0625rem;
  --t-read:   1.1875rem;
  --t-head:   clamp(1.6rem, 2.2vw, 2.05rem);

  --gutter: clamp(1.25rem, 4vw, 3.5rem);
  --measure: 34rem;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

*, *::before, *::after { box-sizing: border-box; }

html {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--work);
  font-size: 100%;
  font-optical-sizing: auto;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  font-size: var(--t-body);
  font-weight: 400;
  line-height: 1.6;
  font-variation-settings: "SOFT" 12, "WONK" 0;
}

/* ------------------------------------------------------------ the record */
/* Every piece of metadata speaks in the machine voice: dates, identifiers,
   counts, capture timestamps, attributions, source links, navigation. */

.record-voice,
.eyebrow, .facts, .chips, .mast nav, footer,
.review h3, .review small, .work-list small, .sequence-nav, .note {
  font-family: var(--record);
  font-size: var(--t-record);
  font-weight: 400;
  letter-spacing: 0.04em;
  font-variation-settings: normal;
}

.eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink-muted);
  margin: 0;
}

.note { color: var(--ink-muted); }

/* ------------------------------------------------------------ typography */

h1, h2, h3 {
  font-weight: 450;
  line-height: 1.18;
  letter-spacing: -0.005em;
  text-wrap: balance;
  margin: 0;
  font-variation-settings: "SOFT" 20, "WONK" 1;
}
h1 { font-size: var(--t-head); }
h2 { font-size: var(--t-read); font-variation-settings: "SOFT" 12, "WONK" 0; }
h3 { font-size: var(--t-body); }

p { margin: 0 0 1em; }
.lede { font-size: var(--t-read); max-width: var(--measure); color: var(--ink); }
.muted, .copyright, small { color: var(--ink-muted); }

/* Links carry no colour. They are marked by a rule that grows from the left. */
a {
  color: inherit;
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 1px;
  transition: background-size 0.3s var(--ease);
  padding-bottom: 0.08em;
}
a:hover { background-size: 100% 2px; }
a.plain, .mast a, .photo-card, .contact-sheet a, footer a { background-image: none; }
.mast a:hover, footer a:hover, .contact-sheet a:hover {
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 100% 1px;
}
a:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

.skip {
  position: fixed; top: -5rem; left: 1rem; z-index: 9;
  background: var(--ink); color: var(--paper); padding: 0.6rem 1rem;
}
.skip:focus { top: 1rem; }

/* ---------------------------------------------------------------- shell  */

.mast {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 2rem; padding: 1.1rem var(--gutter);
  border-bottom: 1px solid var(--line);
}
.mast .name { font-family: var(--work); font-size: var(--t-body); letter-spacing: 0.01em; }
.mast nav { display: flex; gap: 1.25rem; }

footer {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: 1.1rem var(--gutter);
  border-top: 1px solid var(--line);
  color: var(--ink-muted);
}

.wrap { width: min(100% - (2 * var(--gutter)), 82rem); margin-inline: auto; }

/* ------------------------------------------------------------ one screen */
/* Pages whose job is to navigate resolve inside one viewport. min-height,
   never height: a short laptop scrolls rather than clipping content. */

.screen {
  min-height: 100svh;
  display: grid;
  grid-template-rows: auto 1fr auto;
}

.screen > * { min-height: 0; }
.screen-body { min-height: 0; display: grid; align-content: center; gap: clamp(0.75rem, 2.2vh, 1.5rem); padding-block: clamp(0.9rem, 3vh, 2.5rem); }
.screen-head, .screen-foot {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 1rem; padding-block: 1rem;
}
.screen-foot { border-top: 1px solid var(--line); }
.screen-head { border-bottom: 1px solid var(--line); }

/* ------------------------------------------------------------- homepage  */

.portal {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: center;
  min-height: 0;
}
.portrait {
  justify-self: start;
  display: block;
  width: auto; height: auto;
  max-width: 100%;
  max-height: min(58svh, 30rem);
  object-fit: contain;
  /* The cut-out sits on the paper. No frame, no box, no borrowed background. */
}
.portal-text { display: grid; gap: 1.1rem; align-content: center; }
.portal-text .lede { margin: 0; }

/* -------------------------------------------------------------- archive  */

.selection {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(0.75rem, 1.6vw, 1.4rem);
  min-height: 0;
}
.selection a { display: block; min-height: 0; }
.selection img {
  width: 100%; height: 100%;
  max-height: min(30svh, 16rem);
  object-fit: cover;
  background: var(--shade);
}

.columns {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: start;
  min-height: 0;
}

.work-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); }
.work-list li { border-bottom: 1px solid var(--line); }
.work-list a { display: flex; justify-content: space-between; gap: 1.5rem; padding: 0.55rem 0; }

.chips { display: flex; flex-wrap: wrap; gap: 0.5rem 1.25rem; color: var(--ink-muted); }
.facts { display: flex; flex-wrap: wrap; gap: 0.5rem 1.75rem; color: var(--ink-muted); }

/* --------------------------------------------------------- contact sheet */

.contact-sheet {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: clamp(0.4rem, 0.9vw, 0.9rem);
  min-height: 0;
  align-content: center;
}
.contact-sheet a { display: block; min-height: 0; }
.contact-sheet img {
  width: 100%; aspect-ratio: 1; object-fit: cover;
  background: var(--shade);
  filter: grayscale(0.15);
  transition: filter 0.35s var(--ease);
}
.contact-sheet a:hover img { filter: grayscale(0); }

/* ------------------------------------------------------------ photograph */

.plate { display: grid; place-items: center; min-height: 0; }
.art {
  display: block; width: auto; height: auto;
  max-width: 100%; max-height: 100%;
  object-fit: contain;
}

.record-block {
  border-top: 1px solid var(--line);
  padding-top: 0.9rem;
  color: var(--ink-muted);
  max-width: var(--measure);
}

/* ------------------------------------------------------------- reading   */
/* Reading pages are allowed to grow. A 58-response thread is not going into
   a scroll well; the poem composes the first screen and depth follows. */

.reading { max-width: var(--measure); }
.poem {
  white-space: pre-wrap;
  font-size: var(--t-read);
  line-height: 1.8;
  margin: clamp(1.5rem, 4vh, 3rem) 0;
}
.copyright { margin-top: 2.5rem; }

.responses { margin-top: clamp(3rem, 8vh, 6rem); }
.review { max-width: var(--measure); border-top: 1px solid var(--line); padding: 1.1rem 0; }
.review p { white-space: pre-wrap; margin: 0.4rem 0 0.5rem; }
.review h3 { color: var(--ink-muted); }
.response-group { margin-bottom: 4rem; }
.archive-note { border-left: 2px solid var(--line-strong); padding-left: 0.9rem; color: var(--ink-muted); max-width: var(--measure); }

/* -------------------------------------------------------------- motion   */
/* Continuity, not ornament. Cross-document transitions make the archive one
   continuous object instead of a stack of documents. No JavaScript. */

@view-transition { navigation: auto; }

::view-transition-old(root) { animation: fade-out 0.18s var(--ease) both; }
::view-transition-new(root) { animation: fade-in 0.28s var(--ease) both; }

@keyframes fade-out { to { opacity: 0; } }
@keyframes fade-in  { from { opacity: 0; } }

@keyframes settle { from { opacity: 0; transform: translateY(0.35rem); } }
@keyframes draw   { from { transform: scaleX(0); } }

.selection img, .contact-sheet img, .art, .portrait {
  animation: settle 0.55s var(--ease) both;
}
.screen-head, .screen-foot, .work-list, .record-block, .review {
  transform-origin: left;
}
.rule {
  height: 1px; background: var(--line-strong); border: 0;
  transform-origin: left;
  animation: draw 0.7s var(--ease) both;
}

@media (prefers-reduced-motion: reduce) {
  @view-transition { navigation: none; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ------------------------------------------------------------ responsive */
/* The one-screen rule is a desktop commitment. Narrow viewports scroll, which
   is what a phone should do anyway. */

@media (max-width: 900px) {
  .screen { min-height: 0; }
  .portal { grid-template-columns: 1fr; gap: 1.75rem; }
  .portrait { max-height: 42svh; }
  .columns { grid-template-columns: 1fr; }
  .selection { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .selection img { max-height: none; aspect-ratio: 4 / 3; }
  .contact-sheet { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .mast { flex-direction: column; align-items: flex-start; gap: 0.4rem; }
  .mast nav { gap: 1rem; }
  footer { flex-direction: column; gap: 0.35rem; }
}

@media (max-width: 480px) {
  .contact-sheet { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ------------------------------------------------------- opening screen  */
/* Reading pages compose their first screen without claiming the whole page:
   the poem is whole on arrival, the responses continue below it. */

.opening {
  min-height: 68svh;
  display: grid;
  align-content: center;
  padding-block: clamp(1.5rem, 5vh, 3rem);
}
@media (max-width: 900px) { .opening { min-height: 0; } }



/* ------------------------------------------------------ desktop one screen */
/* The one-screen promise is a desktop promise, and it is kept by making the
   shell a flex column so the body row has a genuinely definite height that
   images can resolve against. Below 901px wide or 640px tall the page scrolls,
   which is what a phone should do. Images carry an explicit height because the
   HTML height attribute is a presentational hint: leave it alone and both
   dimensions become definite and aspect-ratio is silently ignored. */

.contact-sheet img, .selection img { height: auto; }

@media (min-width: 901px) and (min-height: 640px) {
  html:has(.screen) { height: 100%; }
  body:has(.screen) { height: 100%; display: flex; flex-direction: column; overflow: hidden; }
  body:has(.screen) main { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }

  .screen { flex: 1 1 auto; min-height: 0; grid-template-rows: auto minmax(0, 1fr) auto; }
  .screen-body { min-height: 0; }

  /* The contact sheet fills the body: six rows of landscape cells. */
  .screen-body:has(> .contact-sheet) { align-content: stretch; grid-template-rows: minmax(0, 1fr); }
  .contact-sheet { height: 100%; grid-template-rows: repeat(6, minmax(0, 1fr)); align-content: stretch; }
  .contact-sheet a { height: 100%; min-height: 0; }
  .contact-sheet img { height: 100%; width: 100%; aspect-ratio: auto; object-fit: cover; }

  /* The archive index: one rank of six, the poem list beneath it. Rows are
     placed explicitly, because hiding any item would otherwise shift every
     following item up a row and scramble the page. */
  .screen-body:has(> .selection) { align-content: stretch; grid-template-rows: auto minmax(0, 1fr) auto auto; }
  .screen-body:has(> .selection) > .lede { grid-row: 1; }
  .screen-body > .selection { grid-row: 2; }
  .screen-body:has(> .selection) > .note { grid-row: 3; align-self: start; }
  .screen-body > .columns { grid-row: 4; align-self: start; }
  .selection { grid-template-columns: repeat(6, minmax(0, 1fr)); height: 100%; }
  .selection a { height: 100%; min-height: 0; }
  .selection img { height: 100%; width: 100%; max-height: none; object-fit: cover; }

  /* The plate is the screen-body itself, so it is sized against the viewport
     rather than against a percentage whose containing block is ambiguous. */
  .screen-body.plate { align-content: center; }
  .art { max-height: calc(100svh - 19rem); max-width: 100%; }
  .mark--full img { max-height: calc(100svh - 24rem); }

  /* The portrait stands on the baseline rather than floating. The cut-out ends
     at the chest, and a severed torso in mid-air reads as damage; met to the
     foot rule it reads as a figure in the room. */
  .screen-body:has(> .portal) { align-content: stretch; grid-template-rows: minmax(0, 1fr); }
  .portal { height: 100%; align-items: end; }
  .portrait { align-self: end; max-height: 100%; }
  .portal-text { align-self: center; padding-bottom: clamp(1rem, 6vh, 4rem); }

  .work-list a { padding: 0.4rem 0; font-size: var(--t-small); }
}

/* --------------------------------------------------------- narrow screens */
/* Mobile scrolls, but it still composes: the header wraps instead of
   colliding, and a photograph is given the room rather than sitting in a band
   of empty paper. */

@media (max-width: 900px) {
  .screen-head, .screen-foot { flex-wrap: wrap; gap: 0.2rem 1rem; }
  .screen { min-height: 100svh; }
  .screen-body.plate { align-content: center; }
  .art { max-height: 68svh; }
  .mast nav { flex-wrap: wrap; }
}

/* Short desktop viewports. The real fonts are slightly taller than the
   fallbacks they were laid out against, and a 720px-tall screen has no slack:
   the poem list and the body gaps tighten rather than the page clipping. */

@media (min-width: 901px) and (max-height: 820px) {
  .work-list a { padding: 0.25rem 0; }
  .work-text { gap: 0.7rem; }
  .practice { gap: 0.35rem; }
  .screen-body.work .mark img { max-height: calc(100svh - 24rem); }
  .screen-body.work .lede { font-size: var(--t-body); }
  .screen-body { gap: 0.6rem; padding-block: 0.75rem; }

  /* A 1fr row will collapse to nothing when its siblings fill the space, and
     the photographs are the last thing on this page that should disappear.
     The rank is given a floor, and the descriptive sentence — the one element
     here that only restates what the page plainly is — gives way instead. */
  .screen-body:has(> .selection) { grid-template-rows: auto minmax(7rem, 1fr) auto auto; }
  .screen-body > .lede { display: none; }
}

/* ------------------------------------------------------------ the mark   */
/* A contour derived from one of the photographs, ink on transparency. It is
   the only ornament on the site, it is made at build time from work that is
   already here, and it changes by editing one line of content-work.json. */

.mark { margin: 0; min-width: 0; min-height: 0; display: grid; place-items: center; }
.mark img {
  display: block; width: 100%; max-width: 100%; height: auto;
  opacity: 0.5;
  animation: settle 0.8s var(--ease) both;
}

/* The 404's mark is a ground the heading sits on. Both occupy one grid cell
   rather than being stacked with absolute positioning: an absolutely placed
   child fills the padding box, which is taller than the content box it is
   measured against, and the page grows by exactly that difference. Grid
   stacking cannot overflow. */
.screen-body.plate { grid-template-areas: "stack"; place-items: center; }
.screen-body.plate > * { grid-area: stack; min-width: 0; min-height: 0; }
.mark--full { width: 100%; display: grid; place-items: center; pointer-events: none; }
.mark--full img {
  width: auto; height: auto;
  max-width: 100%; max-height: min(52svh, 30rem);
  object-fit: contain; opacity: 0.26;
}
.screen-body.plate > h1 { z-index: 1; }

.work { align-content: center; }
.work-text { max-width: var(--measure); display: grid; gap: 1rem; align-content: center; }
.practice { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.55rem; }
.practice li { padding-left: 1.1rem; position: relative; }
.practice li::before {
  content: ""; position: absolute; left: 0; top: 0.72em;
  width: 0.55rem; height: 1px; background: var(--line-strong);
}

@media (min-width: 901px) and (min-height: 640px) {
  .screen-body.work {
    align-content: center;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    grid-template-rows: minmax(0, auto);
    gap: clamp(2rem, 6vw, 5rem);
  }
  .screen-body.work > .work-text { grid-column: 1; grid-row: 1; }
  .screen-body.work > .mark { grid-column: 2; grid-row: 1; min-height: 0; }
  .screen-body.work .mark img { width: auto; max-height: calc(100svh - 22rem); object-fit: contain; }
}

@media (max-width: 900px) {
  .screen-body.work { gap: 1.5rem; }
  .screen-body.work .mark img { max-height: 32svh; width: auto; }
}
