Commit Graph
16 Commits
Author SHA1 Message Date
ukimsanov fed2baf278 docs: correct composition offset rationale; pin nav fix; label-in-name
- html-schema: the previous clause claimed a composition host's data-media-start is
  never read. It is: readElementPlaybackStart (media.ts:16) resolves
  data-playback-start ?? data-media-start and timeline.ts calls it on composition
  clips. Rewrote to the accurate reason both reviewers gave — composition hosts are
  only inspected by the playback-start-first readers, media-start works as a
  fallback, but playback-start is what Studio writes/normalises to.
- motion test: pin the HoverVideo click-suppression (preventDefault +
  stopPropagation). Removing it left the gate 12/12; now it fails. This is the bug
  that escaped static review and only surfaced by driving the live preview.
- replica-compare: fold the visible 'Sound off/on' text into the aria-label so the
  accessible name contains it (WCAG 2.5.3, Rames).

Round-5 findings from Magi and Rames.
2026-08-05 11:32:21 -07:00
ukimsanov 83db94e75e docs(examples): stop the sound control from following the card's link
Grid cards with a public source are wrapped in an <a>; the sound/play button
lives inside it, so a click — mouse or keyboard — bubbled up and navigated to
GitHub instead of toggling sound. preventDefault + stopPropagation on the control.
Found by driving the live preview (self-review).
2026-08-05 11:12:55 -07:00
ukimsanov f3d1d531d1 docs(examples): reset control state on offscreen release; precise a11y labels
- replica-compare: the offscreen teardown now resets muted (element + React state),
  matching HoverVideo — a pair unmuted before it scrolled away no longer returns
  reading 'Sound on' over a paused, sourceless pair (Magi blocker).
- Both controls' aria-labels now follow the mode: under reduced motion the button
  plays/pauses the whole comparison, and when a preview is already autoplaying
  muted the action is 'unmute', not 'play with sound' (self-review + Rames).
- Hardened the motion-suite assertion to scope 'startBoth' to toggleSound's body
  (a defined-but-unused helper no longer satisfies it) and pin the offscreen
  muted-reset transition (Rames mutation-test gap).
- html-schema: 'hyperframes validate' inspects <audio> only; note that no
  media-start-only reader inspects a composition host, so the kind rule strands
  nobody (Rames).
2026-08-05 10:59:29 -07:00
ukimsanov 630c8900f5 docs(examples): reduced-motion play starts both films; precise media-start layers
- replica-compare: the voluntary control now starts and pauses BOTH films (not
  just the reference), and the replica-sync effect attaches in view regardless of
  the preference, so a reduced-motion visitor who presses play sees the whole
  synchronized pair. Added a focused source-level assertion to the motion-check
  suite (the repo has no React runtime harness for snippets).
- html-schema: describe each layer precisely instead of grouping the CLI —
  timing compiler, HTML parser, producer audio, and 'hyperframes validate' read
  only data-media-start; runtime, Studio and 'hyperframes snapshot' read
  data-playback-start first (Studio also writes it).

Round-3 findings from Magi.
2026-08-05 10:41:34 -07:00
ukimsanov c9fd7465c9 docs(examples): allow voluntary playback under reduced motion; a11y parity
Both snippet players now keep a source assigned whenever in view (preload='none'
so nothing downloads until asked), gating only autoplay on reduced motion — so a
reduced-motion visitor can press play/unmute instead of being left with an inert
poster. HoverVideo renders a control on hasAudio={false} cards too (a play/pause
toggle), and ReplicaCompare no longer calls play() on a source it just removed.
Gave ReplicaCompare's button the same aria-pressed + focus-visible as HoverVideo.
Round-2 finding from Magi (#1) and Rames.
2026-08-05 10:17:40 -07:00
ukimsanov aa077e000d docs(examples): intersection-gate the recreate pair too
ReplicaCompare now assigns/decodes its reference+replica only near the viewport
and releases both (pause + removeAttribute + load) on exit, closing the last
eager-load surface from Magi's #5. Sync + reduced-motion guard preserved.
2026-08-05 05:54:49 -07:00
ukimsanov 0467bc8129 docs(examples): make hover-audio accessible, lazy-loaded, and guard-complete
HoverVideo now: (1) exposes a focusable button that toggles/plays sound by
keyboard, touch and pointer — hover is an enhancement, not the only path;
(2) gates the source behind an IntersectionObserver so offscreen cards no longer
download/decode, releasing the buffer on exit; (3) still blocks autoplay under
reduced motion while allowing voluntary playback via the button. Routed all 30
Thirty-Days cards through it (hasAudio={false} for the 12 silent ones) so no raw
<video autoPlay> bypasses the motion guard. Flagged by Magi (P1 #4/#5/#6).
2026-08-05 05:48:23 -07:00
ukimsanov 5361b9e10c docs(30-days): hover a card to hear the film's audio
Adds a HoverVideo snippet (autoplay muted, unmute + play on hover, re-mute on
leave) and points the 18 days whose source has real audio at new -s.mp4 keys
re-encoded with sound (CDN is immutable, so new keys not overwrites). The other
12 days have genuinely silent sources and stay muted. Honours prefers-reduced-motion.
2026-08-05 00:36:07 -07:00
ukimsanov 407a5fd513 docs(examples): bind reference video muted to state so unmute sticks
React was re-applying the static muted={true} on every re-render, instantly
re-muting the reference after a click. Bind muted to state instead.
2026-08-05 00:12:12 -07:00
ukimsanov bde03484ae docs(examples): sync the recreate pairs with a tap-to-unmute button
The reference and replica play on one shared clock; a single Instagram-style
button unmutes the reference (its original audio), the replica stays silent.
Reference videos re-encoded with audio on the CDN. The component honours
prefers-reduced-motion on both edges, per the docs-snippet-motion guard.
2026-08-05 00:04:26 -07:00
ukimsanov f742084fa8 docs: put the ten workflows in one place, and connect the two tracks
There are ten creation workflows. The Workflows nav group held eight of them plus
`guides/export-and-share`, which is not a workflow at all — there is no such
skill, it is a finishing task. The tenth, the Remotion port, sat under
Developers > Composition, design & animation, a different tab entirely.

So: export-and-share moved to Build the project where the other task pages live,
the Remotion page moved into Workflows, and the chooser gained a tenth route. The
chooser rendered `<video>` unconditionally, which would have drawn a black
rectangle for a route with no clip — it now renders a neutral tile instead, and
custom.css styles it to the same box.

**The Prompt Guide and the workflow guides did not know about each other.** Level 1
is organised one page per workflow, and five of its six pages linked to no guide
at all. A reader learning to prompt for motion graphics never discovered
/guides/motion-graphics, and the traffic went nowhere in both directions. Each
Level 1 page now names its workflow and says what that page adds.

Four links pointed at redirects rather than the real page — /guides/claude-design,
/guides/common-mistakes, /guides/website-to-video, /guides/media-overlays. Seven
files now link to the destination directly.

Checked after: 0 broken links, 0 links via redirect.
2026-08-04 15:32:14 -07:00
ukimsanov 60f09dfcfd docs: give every click-to-play video an audio track
Ten videos on the docs site played in silence. The player is click-to-play, so a
reader deliberately starts them and gets nothing — which reads as broken rather
than as a stylistic choice.

The music is from HeyGen's own catalogue, pulled through the media-use skill's
resolve path with the account already authenticated on this machine. That matters
more than convenience: the launch projects on disk carry a `launch-music-trimmed.mp3`
with no licence, no attribution and no metadata, and three Pixabay tracks. Neither
belongs on the official docs CDN, where objects cache immutably for a year.

Mixed at roughly -22 to -24 dB mean so it sits under the picture. Getting there
took two corrections worth recording: proportional fades, because a 1.2s fade-out
on a 5s clip eats half the audio; and seeking 11s into the bed, because its first
ten seconds are a quiet intro that dragged every short clip down to -27 dB.

The three Examples tiles are left silent on purpose — they are muted autoplay
loops, so an audio track would only be weight.

Verified: every video referenced anywhere under docs/ that is not a muted
autoplay tile now has an AAC stream, checked against the live CDN URL rather than
the local file.
2026-08-04 03:08:04 -07:00
Ular Kimsanov edfe66a953 docs: add the shared page components and the Reference Project (#2977)
* docs: add the shared page components

Adds the six React snippets the rebuilt documentation pages compose against,
plus the styles they need. Nothing imports them yet, so this lands with no
user-visible change and no navigation churn.

- DocsVideo / ShowcaseWall — the film player and the Showcase grid
- LiveReferenceProject — embeds the Reference Project via <hyperframes-player>
- WorkflowChooser, AgentAction, and the two grid snippets

The scrub indicator is a timecode bubble rather than a thumbnail. Mounting a
second <video> with the same src to drive a preview frame made every page
carrying a film download the whole file twice, which is not worth a thumbnail.

* docs: add the Reference Project example

One real 10-second project the documentation can point at instead of describing
a hypothetical one: a live capture of example.com, synthesised narration, and
caption timings measured from that narration. It passes its own gates —
`hyperframes lint` clean, `hyperframes check` passed, 28/28 text checks WCAG AA.

No page imports it yet, so this lands without touching navigation.

Only the two WAV masters exceed the repository's 500 KB non-LFS limit, so only
those go through LFS. The MP3 stings and the capture PNG stay plain, which keeps
the example usable after a clone without `git lfs pull`.

`bun run docs:bundle-reference` regenerates the single-file embed the
Introduction page loads from the CDN.

* docs: keep the Reference Project verification report

The Examples page links this file twice — as "What changed after review" and
as "The real verification report" — in the section that makes the project's
brief, source, revision notes, and checks public end to end. It is a published
artifact, not leftover scaffolding.

* docs: state the Reference Project embed's isolation contract

The composition is fetched from the CDN and handed to the player as a blob:
URL, which inherits the docs origin, and <hyperframes-player> sandboxes its
iframe with allow-scripts + allow-same-origin. So the embedded composition runs
with script access to this origin.

That is a consequence of how the player works — it drives seeking through the
iframe's document, which a cross-origin frame does not expose — not something
this component can fix. Serving the CDN URL directly would isolate the frame
and break playback.

The guard is therefore the source, so the comment says so out loud: src must
stay a first-party path we publish, never user- or community-supplied HTML.

* fix(docs): resolve reduced-motion on the first render, and the embed's dep gap

Both defects from Rames Jusso's review on #2977. Neither is visible today
because nothing imports these files yet, which is what makes them cheap now.

**Reduced motion resolved one paint too late, in all three grids.**
`useState(false)` plus a `matchMedia` read in an effect meant the first
committed render always emitted `<video src autoPlay loop>`; a reduce-motion
visitor had 6 + 8 + 4 tiles already fetching before the attributes came off.
`autoPlay` also overrides `preload="metadata"`, so those were the files, not
metadata probes — and dropping `src` with no following `load()` is not a
reliable abort. A lazy initializer knows the answer on the first render.

**LiveReferenceProject never sent the initial variables.** The sending effect
read `playerRef.current`, assigned by the effect above it on the commit where
`compositionSrc` lands — a commit with nothing in the sending effect's dep
array. So it ran once against a null ref and never again. It looked correct
only because the three defaults match what the composition already renders.

Also from the same review:

- The object URL could outlive its revoke: once the body resolves, `abort()`
  no longer stops the chain, so the blob could be minted after cleanup ran with
  `objectUrl` still undefined. Same `cancelled` guard the effect above uses.
- `postMessage` targeted `"*"` while the isolation comment argues the frame is
  same-origin. Naming `window.location.origin` turns that prose guard into an
  enforced one.
- Nothing reached a terminal state when the player script never arrived:
  `whenDefined()` does not reject, and a later mount reuses the tag without its
  error listener. A CSP rule or content blocker never fires `error` at all.
  A deadline covers every path instead of sitting on "Loading…" forever.
- `loadFailed` was never cleared, so one transient failure stuck.
- The README claimed a clone works without `git lfs pull`. It does for the
  visuals; both WAVs are pointers and they are the bed and the voiceover, so
  the captions would play over silence. Says so now.
- The bundler stripped trailing whitespace document-wide while inlining the
  runtime, which reaches inside script template literals where those spaces are
  data. It also assumed a literal `<head>` and would silently ship an embed with
  no `<base>`. Strip removed, anchor asserted.

Copilot's five "missing hook imports" comments are wrong — Mintlify pre-injects
the hooks, and `TemplateCard.jsx`, cited as the counter-example, uses the
`export function` form the same page says is unsupported.

* fix(docs): stop preview loops when Reduce Motion is turned on mid-session

Miguel's changes-requested on #2977. He is right about the mechanism: dropping
`src` and `autoPlay` through React props neither pauses a playing element nor
aborts its selected resource, so a visitor who turned Reduce Motion on with the
page already open kept every tile running.

Measured in a browser rather than argued from the spec, same clip, same
sequence:

  playing                 paused=false  t=2.90  readyState=4  networkState=1
  React props only        paused=false  t=3.90  readyState=4  networkState=1
  + pause/removeAttr/load paused=true   t=0     readyState=0  networkState=0

The middle row is the bug: time still advancing, resource still held.

Rames' follow-up asked for a remount-to-poster instead, because a video that
ends with `src` removed holds its last frame and `poster` only paints before
playback begins. `load()` covers that too — it drops readyState to
HAVE_NOTHING, which is precisely the state that paints the poster. Confirmed
side by side on screen: the React-props-only tile sits on an arbitrary mid-clip
frame, the pause/load tile shows the poster again. So no remount is needed.

The guard cannot be shared as code — Mintlify compiles each snippet in
isolation and forbids one importing another — so it is copy-pasted into all
three grids. A duplicated invariant is the kind that rots, and a rendering test
would mean adding React to a repo that only carries it inside packages/studio,
plus mocking Mintlify's hook-injection contract with a mock that can stay green
while the page breaks. `scripts/check-docs-snippet-motion.mjs` asserts the
source instead, wired into `bun run lint`, with unit tests covering both edges.

That gate immediately found `docs/snippets/TemplateCard.jsx`: autoplays with no
reduced-motion handling at all. It is imported by zero pages, and it uses the
`export function` form Mintlify's constraints page says is unsupported, so it
would not work if it were. Deleted rather than fixed.

* refactor(scripts): split the motion guard into named predicates

fallow flagged findMotionGuardViolations at CRAP 42 — a finding this branch
introduced, so it gets fixed rather than suppressed, same as the catalog
generator earlier in the stack.

The two conditions are now their own predicates behind a small requirements
table, which drops the branch count under the threshold and makes each rule
readable on its own line. Same output, same tests.

* fix(docs): move the stop effect above ShowcaseWall's early return

Rames' changes-requested on `e1a03c63`. The effect I added in the previous
commit landed below `if (open) return`, so `ShowcaseWall` called five hooks on
the grid render and four once a tile was open. That is a conditional hook:
clicking a tile — the component's primary interaction — threw "Rendered fewer
hooks than expected".

Worth naming why it landed in one of three. `workflow-chooser` and
`advanced-path-grid` have no early return, so the same paste position was fine
there. `ShowcaseWall` is the only one with a conditional return and it got the
same copy. That is the duplication cost this script's own header warns about,
showing up in the commit that added the script.

**The bespoke gate could not have caught it, and now the generic one does.**
`.oxlintrc.json` already loaded the `react` plugin and never excluded `docs/`
— only `.prettierignore` does, which is why formatting is not a finding here
but linting reaches these files. Naming the two hook rules in an override
scoped to `docs/snippets/**` reports this bug directly, and also reports the
`compositionSrc` dependency gap from round one that was found by reading.
Verified both ways: reintroducing the conditional hook produces
`react-hooks(rules-of-hooks)`, and `bunx oxlint .` is clean repo-wide, so
nothing lit up in `packages/studio`.

**Two holes in the script itself, both from the same review.**

It matched whole files while the invariant is per component, so a second
unguarded grid in `docs-video.jsx` would have ridden in on `ShowcaseWall`'s
guard. It now splits by component. That immediately surfaced the distinction
between a component that decides to autoplay and one that forwards its caller's
`autoPlay` prop — `DocsVideo` only ever plays because a reader clicked, so it
does not owe a preference check.

And `readsPreferenceLazily` never tied its halves: any lazy initializer plus
the media-query string anywhere in the file passed, which is the original bug
satisfying the check written to prevent it. The query now has to sit inside the
initializer's own expression.

Both holes have tests. fallow is clean at 0 introduced.

* fix(scripts): close the two silent gaps in the motion gate

Both from Rames' approval pass on #2977, and both found by running these
functions rather than reading them. Both fail the same quiet way: a component
`autoplays` misses is filtered out before any requirement runs, so the gate
reports zero problems instead of a violation.

`autoplays` had become narrower than the version it replaced. Excluding the
`autoPlay={autoPlay}` passthrough was right, but the replacement only matched
`autoPlay={` or `autoPlay` alone on a line, so `<video autoPlay muted />` on one
line slipped through. Restored the old breadth. Two things are stripped first
rather than one — the passthrough, and the prop's own default in the signature,
which is a declaration and not a use. Without the second strip, `DocsVideo` is
asked to own a decision it only forwards.

`splitComponents` anchored on `^export`, so anything not exported folded into
the previous exported component and inherited its guard. Same hole as the
whole-file match, narrowed from file scope to non-export scope. The anchor no
longer requires `export`.

Ten tests now, including his exact examples for both.

* docs: remove the live-composition embed and its build apparatus

The Introduction no longer carries the embed (removed in #2979), and nothing
else used any of this: the 200-line snippet, 26 CSS rules, the bundler that
built the single-file HTML for the CDN, its npm script, and the README section
explaining how to regenerate it.

The Reference Project itself stays — Examples, Developers, and Go further all
link to it as the worked example; only the interactive embed of it is gone.

This also retires the isolation contract I documented two rounds ago. That
comment existed because the embed handed CDN HTML to a same-origin blob; with
the embed gone there is no such surface to reason about, which is a better
outcome than a comment explaining why it was acceptable.

* docs: remove the AgentAction snippet

Its only consumer is gone. The Quickstart now shows the agent instruction in a
plain fence instead, because this component rendered a Copy button and never
displayed the request — a reader copied text they could not read, which is the
wrong shape for the one affordance a non-technical visitor depends on.

Mintlify fences already carry a copy button and show their contents.
2026-08-04 00:37:48 -07:00
James RussoandClaude Opus 4.7 4ae5c0340f chore(docs): migrate docs/images/ media to static.heygen.ai CDN (#301)
Move all preview mp4/png/gif assets under docs/images/ out of the repo
and serve them from https://static.heygen.ai/hyperframes-oss/docs/images/
(backed by s3://heygen-public/hyperframes-oss/docs/images/, CloudFront).

Drops ~49MB from the working tree and, more importantly, ~49MB from every
future Mintlify build checkout. Combined with the (already-LFS-tracked)
producer snapshots, the remaining bloat in 'npx skills add heygen-com/
hyperframes' (see #300) is LFS smudge during clone — separate fix needed
in the skills CLI to pass GIT_LFS_SKIP_SMUDGE=1.

Changes:
- Delete docs/images/** (103 files, ~49MB). Files are uploaded to S3 already.
- Rewrite /images/* references in 44 MDX files, TemplateCard.jsx, and
  catalog-index.json to absolute CDN URLs.
- Update README.md img src to CDN URL (renders correctly on GitHub).
- Add docs/images/ to .gitignore so regenerated previews aren't committed.
- Add scripts/upload-docs-images.sh to sync docs/images/ → S3 after running
  the preview generators.
- Wire up bun run upload:docs-images and bun run generate:catalog-previews
  scripts in package.json.
- Update generator script docstrings to point at the upload step.

External contributors can still regenerate previews locally (mintlify dev
reads the CDN URLs, so broken previews appear only for newly added items
pending a maintainer upload). Maintainers run:
  bun run generate:catalog-previews --only <name>
  bun run upload:docs-images

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 23:13:43 -07:00
James Russo 08fb1de61f feat(cli): add command + hyperframes.json (#256)
## What

PR 5/17 of the catalog system rollout. Adds the `hyperframes add` verb for installing blocks and components from the registry into an existing project, plus the `hyperframes.json` project config that tells `add` which registry to use and where to drop files. Stacks on #255.

- **`packages/cli/src/commands/add.ts`** — new `hyperframes add <name>` command. Resolves an item, validates target paths, installs files in parallel, builds an include snippet, copies it to the clipboard. Exposes a testable `runAdd(opts)` function; the citty default wraps it with console output + exit handling
- **`packages/cli/src/utils/projectConfig.ts`** — read/write/normalize `hyperframes.json`. Tolerant to missing and partial configs
- **`packages/cli/src/utils/clipboard.ts`** — minimal cross-platform clipboard (pbcopy / clip.exe / wl-copy / xclip / xsel). Zero deps. Gracefully no-ops in headless environments
- **`packages/cli/src/commands/init.ts`** — write `hyperframes.json` during scaffold if not already present
- **`packages/cli/src/cli.ts`** + **`help.ts`** — register `add` under Getting Started (directly below `init`)

Design doc: [Hyperframes Catalog System](https://www.notion.so/heygen/Hyperframes-Catalog-System-Design-Plan-341449792c69813f899dcd53b4c0383a).

## UX

```bash
# Scaffold a project (now writes hyperframes.json too)
npx hyperframes init my-video --example blank
cd my-video

# Add a block — files land, snippet copied to clipboard
npx hyperframes add claude-code-window
#  ✓ Added claude-code-window (hyperframes:block)
#    compositions/claude-code-window.html
#
#  Include snippet:
#    <iframe src="compositions/claude-code-window.html" data-start="0" data-duration="6"></iframe>
#
#  Copied to clipboard — paste into your host composition.

# Add a component effect
npx hyperframes add shader-wipe

# Headless / CI — no clipboard, JSON output for tooling
npx hyperframes add shader-wipe --no-clipboard --json
```

Running `hyperframes add warm-grain` (an example) errors clearly pointing to `init --example`.

## Docs (bundled in this PR per the tracker principle)

- `docs/packages/cli.mdx` — new `add` subsection under Commands (flags, examples, trigger rules) + new `hyperframes.json` section describing the config file shape

## Tests

- **`packages/cli/src/commands/add.test.ts`** — 11 tests:
  - `remapTarget` / `buildSnippet` pure helpers (5 tests)
  - `runAdd` integration against a mocked `fetch` registry: block install lands files + returns snippet, component install respects `paths.components` remap, example-typed names throw `AddError` with code `example-type`, unknown names throw `AddError` with code `unknown-item` (4 tests plus 2 covering block default path and non-default path preservation)
- **`packages/cli/src/utils/projectConfig.test.ts`** — 9 tests:
  - Write/read round-trip, partial-config normalization, corrupt-file handling, absent-file fallback to defaults, custom paths preserved
- **CLI suite:** 92 passed (was 72 on #255, **+20**). Same 4 pre-existing failures unchanged

## Scope decisions

- **`init.ts` full port to new resolver deferred.** The original plan bundled a removal of the `packages/cli/src/templates/` compat shim. That's ~300 more lines and isn't required for `add` to work. The compat shim from #254 still functions; a separate cleanup PR handles it
- **No ajv runtime schema validation.** Manifests are trusted as schema-valid. Full validation lands when third-party registries arrive (PR 14/15). Path safety is still enforced by the installer's `assertSafeTarget` guard
- **Default project paths stay under `compositions/`.** Blocks → `compositions/<name>.html`; components → `compositions/components/<name>/<file>`. Users override via `hyperframes.json#paths`

## Breaking / migration

**None.** Pure additive — new command, new file types, no existing commands or flags change. `init.ts` now writes `hyperframes.json` but that's a new additional file, not a modification of existing output.

## Stacks on

#255 — base branch. When #255 merges, this rebases onto `main`.

## Next in stack

PR 6 — `feat(registry): seed block — claude-code-window`. First real registry item. Exercises the full `hyperframes add <name>` flow end-to-end against a committed item on `main`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-13 21:04:59 -07:00
James Russo fee51f7a65 feat(docs): add template gallery page with visual previews (#160)
* feat(docs): add template gallery page with visual previews

* fix(docs): remove invalid MDX heading anchors

* chore: retrigger CI

* feat(docs): merge gallery into templates page with hover-to-play video previews

- Consolidated gallery.mdx and templates.mdx into single templates.mdx
- Moved templates page to Getting Started section
- Added MP4 video previews rendered by hyperframes (hover to play)
- Custom JS for hover-to-play behavior (Mintlify strips JSX event handlers)
- 2-column grid for landscape, 3-column for portrait
- Remotion-style cards with gradient overlay labels

* fix(docs): update broken links after templates page move

* ci(regression): remove scripts/ from regression trigger paths

scripts/ contains dev utilities (lint, versioning, preview generation)
that don't affect the rendering engine.
2026-03-31 13:04:04 -07:00