]> git.plutz.net Git - rawnet/commitdiff
Merge commit 'c4c012d9542c4b266fa701c3aced3b97f11a4797'
authorPaul Hänsch <paul@plutz.net>
Wed, 29 Sep 2021 21:51:24 +0000 (23:51 +0200)
committerPaul Hänsch <paul@plutz.net>
Wed, 29 Sep 2021 21:51:24 +0000 (23:51 +0200)
17 files changed:
.gitignore
Makefile [new file with mode: 0644]
cgilite/.gitignore [new file with mode: 0644]
cgilite/cgilite.sh [moved from cgilite.sh with 100% similarity]
cgilite/common.css [moved from common.css with 100% similarity]
cgilite/file.sh [moved from file.sh with 100% similarity]
cgilite/html-sh.sed [moved from html-sh.sed with 100% similarity]
cgilite/logging.sh [moved from logging.sh with 100% similarity]
cgilite/markdown.awk [moved from markdown.awk with 100% similarity]
cgilite/session.sh [moved from session.sh with 100% similarity]
cgilite/storage.sh [moved from storage.sh with 100% similarity]
cgilite/users.sh [moved from users.sh with 100% similarity]
index.cgi [new file with mode: 0755]
page_404.sh [new file with mode: 0755]
page_channel.sh [new file with mode: 0755]
page_video.sh [new file with mode: 0644]
rawnet.css [new file with mode: 0644]

index 5c9950ae1c74eb4a290c3887873f567f85c3de4d..e3c57b41d263e10579e1ee021547c205327d796b 100644 (file)
@@ -1,3 +1,3 @@
-cgilite
-serverkey
+channels.db
 users.db
+serverkey
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..24781a9
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,9 @@
+.PHONY: _subtrees
+
+_subtrees: _cgilite
+
+cgilite:
+       git subtree add --squash -P $@ https://git.plutz.net/git/$@ master
+
+_cgilite: cgilite
+       git subtree pull --squash -P $< https://git.plutz.net/git/$< master
diff --git a/cgilite/.gitignore b/cgilite/.gitignore
new file mode 100644 (file)
index 0000000..5c9950a
--- /dev/null
@@ -0,0 +1,3 @@
+cgilite
+serverkey
+users.db
similarity index 100%
rename from cgilite.sh
rename to cgilite/cgilite.sh
similarity index 100%
rename from common.css
rename to cgilite/common.css
similarity index 100%
rename from file.sh
rename to cgilite/file.sh
similarity index 100%
rename from html-sh.sed
rename to cgilite/html-sh.sed
similarity index 100%
rename from logging.sh
rename to cgilite/logging.sh
similarity index 100%
rename from markdown.awk
rename to cgilite/markdown.awk
similarity index 100%
rename from session.sh
rename to cgilite/session.sh
similarity index 100%
rename from storage.sh
rename to cgilite/storage.sh
similarity index 100%
rename from users.sh
rename to cgilite/users.sh
diff --git a/index.cgi b/index.cgi
new file mode 100755 (executable)
index 0000000..9beb6a9
--- /dev/null
+++ b/index.cgi
@@ -0,0 +1,116 @@
+#!/bin/sh
+
+USER_REGISTRATION=false
+USER_REQUIREEMAIL=false
+
+. "${_EXEC:-${0%/*}}"/cgilite/cgilite.sh
+. "$_EXEC"/cgilite/session.sh nocookie
+. "$_EXEC"/cgilite/users.sh
+
+PATH_INFO="$(PATH "/${PATH_INFO#${_BASE}}")"
+
+export MD_HTML="false"
+if [ "$(which awk)" ]; then
+  markdown() { awk -f "$_EXEC/cgilite/markdown.awk"; }
+else
+  markdown() { busybox awk -f "$_EXEC/cgilite/markdown.awk"; }
+fi
+
+checked(){
+  local check="$1"; shift 1;
+  for comp in "$@"; do
+    if [ "$check" = "$comp" ] || [ "$check" -eq "$comp" ]; then
+      printf 'checked="checked"'
+      break;
+    fi 2>/dev/null
+  done
+}
+selected(){
+  local check="$1"; shift 1;
+  for comp in "$@"; do
+    if [ "$check" = "$comp" ] || [ "$check" -eq "$comp" ]; then
+      printf 'selected="selected"'
+      break;
+    fi 2>/dev/null
+  done
+}
+
+w_user_login(){
+  if [ ! "$USER_ID" ]; then
+    cat <<-EOF
+       [form #user_login .login method=POST
+         [label Login]
+         [input name=uname placeholder="Username or Email" autocomplete=off]
+         [input type=password name=pw placeholder="Passphrase"]
+         [submit "action" "user_login" Login]
+         $([ "$USER_REGISTRATION" = true ] && printf '[a href="%s/register/" Register]' "$_BASE")
+       ]
+       EOF
+  elif [ "$USER_ID" ]; then
+    cat <<-EOF
+       [form #user_login .logout method=POST
+         [p Logged in as [span . $(HTML ${USER_NAME})]]
+         $([ "$USER_REGISTRATION" != true ] && printf '[a href="%s/invite/" Invite Friend]' "$_BASE")
+         [submit "action" "user_logout" Logout]
+       ]
+       EOF
+  fi
+}
+
+yield_page(){
+  title="${1:-RAW:NET}" page="$2"
+  printf '%s\r\n' 'Content-Type: text/html; charset=utf-8' \
+                  "Content-Security-Policy: script-src 'none'" \
+                  ''
+  { cat <<-EOF
+       [!DOCTYPE HTML]
+       [html [head
+         [meta name="viewport" content="width=device-width"]
+         [link rel="stylesheet" type="text/css" href="$_BASE/cgilite/common.css"]
+         [link rel="stylesheet" type="text/css" href="$_BASE/rawnet.css"]
+         [title . $(HTML "$title")]
+       ] [body class="$page"
+         [header
+           [form method=POST action="$_BASE/search/"
+             [input name=search placeholder="Search"]
+           ]
+           $(w_user_login)
+         ][main
+       EOF
+  cat
+  printf ']]]'
+  } |"$_EXEC/cgilite/html-sh.sed" -u
+}
+
+case ${PATH_INFO} in
+  /favicon.ico) printf '%s\r\n' 'Content-Length: 0' '';;
+  *.css)
+    . "${_EXEC}/cgilite/file.sh"
+    FILE "${_EXEC}/${PATH_INFO}"
+    ;;
+  /login/)
+    if [ "$USER_ID" ]; then
+      REDIRECT "${_BASE}/"
+    else
+      yield_page 'RAW:NET Login' login <<-EOF
+       $(w_user_login)
+       EOF
+    fi
+    ;;
+  /register/)
+    yield_page 'RAW:NET Register User' register <<-EOF
+       $(w_user_register)
+       EOF
+    ;;
+  /recover/)
+    yield_page 'RAW:NET Recover Account' recover <<-EOF
+       $(w_user_recover)
+       EOF
+    ;;
+  /|/channel/*) . "${_EXEC}/page_channel.sh";;
+  /playlist/*) . "${_EXEC}/page_playlist.sh";;
+  /search/*) . "${_EXEC}/page_search.sh";;
+  *) . "${_EXEC}/page_404.sh";;
+esac
+
+exit 0
diff --git a/page_404.sh b/page_404.sh
new file mode 100755 (executable)
index 0000000..a4acce8
--- /dev/null
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+printf 'Status: 404 Not Found\r\n'
+
+yield_page '' 404 <<-EOF
+[h1 404][p
+[span Page not found or nevermore]
+[span Quoth the server: 404]
+]
+EOF
diff --git a/page_channel.sh b/page_channel.sh
new file mode 100755 (executable)
index 0000000..57de476
--- /dev/null
@@ -0,0 +1,223 @@
+#!/bin/sh
+
+chan_db="$_DATA/channels.db"
+
+channel='' video='' action=''
+path_info="$PATH_INFO"
+path_info="${path_info#/channel/}"
+if [ "$(checkid "${path_info%%/*}")" ]; then
+  channel="${path_info%%/*}"
+  path_info="${path_info#*/}"
+fi
+if [ "$(checkid "${path_info%%/*}")" ]; then
+  video="${path_info%%/*}"
+  path_info="${path_info#*/}"
+fi
+action="${path_info}"
+unset path_info
+
+# Channel
+# ID   NAME    DESCRIPTION     LOGO    THEME   AUTHORS DESCR_CACHE FUTUREUSE
+
+if [ "$channel" -a -f "$chan_db" -a -r "$chan_db" ]; then
+  read -r CHANNEL_ID CHANNEL_NAME CHANNEL_DESCRIPTION CHANNEL_LOGO \
+          CHANNEL_THEME CHANNEL_AUTHORS CHANNEL_DESCR_CACHE \
+          CHANNEL_FUTUREUSE <<-EOF
+       $(grep "^${channel}     " "${chan_db}")
+       EOF
+  if [ "$CHANNEL_ID" ]; then
+           CHANNEL_NAME="$(UNSTRING "${CHANNEL_NAME}")"
+    CHANNEL_DESCRIPTION="$(UNSTRING "$CHANNEL_DESCRIPTION")"
+        CHANNEL_AUTHORS="$(UNSTRING "$CHANNEL_AUTHORS")"
+    CHANNEL_DESCR_CACHE="$(UNSTRING "$CHANNEL_DESCR_CACHE")"
+    vid_db="${_DATA}/${CHANNEL_ID}/videos.db"
+  else
+    channel=''
+  fi
+fi
+
+update_channel(){
+  local id="${1}" name="${2}" description="${3}" logo="${4}" theme="${5}" \
+        authors="${6}" descr_cache="${7}" futureuse="${8}"
+  local ID INFO
+  if LOCK "$chan_db"; then
+    while read -r ID INFO; do
+      if [ "$id" = "$ID" ]; then
+       printf '%s      %s      %s      %s      %s      %s      %s      %s\n' \
+               "$id" "$(STRING "$name")" "$(STRING "$description")" \
+               "${logo:-\\}" "${theme:-\\}" "$(STRING "$authors")" \
+               "$(printf %s "$description" |markdown |STRING)" \
+               "${futureuse:-\\}"
+      else
+       printf '%s      %s\n' "$ID" "$INFO"
+      fi
+    done <"$chan_db" >"${chan_db}.$$"
+    mv -- "${chan_db}.$$" "${chan_db}"
+    RELEASE "$chan_db"
+  else
+    return 1
+  fi
+}
+
+# Video
+# ID   NAME    DESCRIPTION     RESX    RESY    LENGTH  COVER   STATUS  UPLOADER        HITS
+
+[ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in
+  newchannel)
+    channel="$(POST channel |checkid)"
+    if [ ! "$USER_ID" ]; then
+      REDIRECT "${_BASE}/channel/#ERROR_NEWCHANNEL_NOTALLOWED"
+    elif LOCK "$chan_db"; then
+      if grep -q '^${channel}  ' "$chan_db"; then
+        RELEASE "$chan_db"
+        REDIRECT "${_BASE}/channel/#ERROR_NEWCHANNEL_EXISTS"
+      else
+       printf '%s      \\      \\      \\      \\      %s      \\      \\\n' \
+               "$channel" "$(STRING "$USER_ID")" \
+               >>"$chan_db"
+        RELEASE "$chan_db"
+        REDIRECT "${_BASE}/channel/${channel}/edit"
+      fi
+    else
+      REDIRECT "${_BASE}/channel/#ERROR_NEWCHANNEL_NOLOCK"
+    fi
+    ;;
+  update_channel)
+    if [ ! "$channel" ]; then
+      REDIRECT "${_BASE}/channel/#ERROR_NOCHANNEL"
+    elif [ ! "$USER_ID" ]; then
+      REDIRECT "${_BASE}/channel/${channel}/#ERROR_NOTLOGGEDIN"
+    elif [ "${CHANNEL_AUTHORS##*${USER_ID}*}" ]; then
+      REDIRECT "${_BASE}/channel/${channel}/#ERROR_UPDATE_NOTALLOWED"
+    elif update_channel "$channel" "$(POST name)" "$(POST description)" \
+                        "" "" "$USER_ID" "" ""; then
+      REDIRECT "${_BASE}/channel/${channel}/"
+    else
+      REDIRECT "${_BASE}/channel/${channel}/#ERROR_UPDATE_NOLOCK"
+    fi
+    ;;
+  update_channel_cancel)
+    REDIRECT "${_BASE}/channel/${channel}/"
+    ;;
+  newvideo)
+    video="$(POST video |checkid)"
+
+    [ "$channel" -a "$USER_ID" -a ! "${CHANNEL_AUTHORS##*${USER_ID}*}" ] \
+    && mkdir -p -- "${_DATA}/${channel}/"
+
+    if [ ! "$video" ]; then
+      REDIRECT "${_BASE}/channel/${channel}/#ERROR_INVALID_ID"
+    elif [ ! "$channel" ]; then
+      REDIRECT "${_BASE}/channel/#ERROR_NOCHANNEL"
+    elif [ ! "$USER_ID" ]; then
+      REDIRECT "${_BASE}/channel/${channel}/#ERROR_NOTLOGGEDIN"
+    elif [ "${CHANNEL_AUTHORS##*${USER_ID}*}" ]; then
+      REDIRECT "${_BASE}/channel/${channel}/#ERROR_UPDATE_NOTALLOWED"
+    elif LOCK "$vid_db"; then
+      if grep -q '^${video}    ' "$vid_db"; then
+        RELEASE "$vid_db"
+        REDIRECT "${_BASE}/channel/${channel}/#ERROR_NEWVIDEO_EXISTS"
+      else
+               # ID    NAME    DESC    RESX    RESY    LENGTH  COVER   STATUS  UPLOADER HITS   FUTUREUSE
+       printf '%s      \\      \\      \\      \\      \\      \\      private %s      \\      \\\n' \
+               "$video" "$(STRING "$USER_ID")" \
+               >>"$vid_db"
+        RELEASE "$vid_db"
+        REDIRECT "${_BASE}/channel/${channel}/${video}/edit"
+      fi
+    else
+      REDIRECT "${_BASE}/channel/${channel}/#ERROR_NEWVIDEO_NOLOCK"
+    fi
+    ;;
+esac
+
+w_video(){
+  local CID="$1" thumb
+  local ID NAME DESCRIPTION RESX RESY LENGTH COVER STATUS UPLOADER HITS DESCR_CACHE FUTUREUSE
+  if read -r ID NAME DESCRIPTION RESX RESY LENGTH COVER STATUS UPLOADER HITS FUTUREUSE; then
+    thumb="${_BASE}/${CID}/thumb_${ID}.jpg"
+    [ "$NAME" = \\ ] && NAME="(Unnamed Video)"
+    cat <<-EOF
+       [div .video
+         [h3 [a href="${ID}/" . $(UNSTRING "$NAME" |HTML)]]
+         [img href="${thumb}" alt="$(UNSTRING "$DESCR_CACHE")"]
+       ]
+       EOF
+  else
+    return 1
+  fi
+}
+
+w_channel(){
+  local vid_db
+  local ID NAME DESCRIPTION LOGO THEME AUTHORS DESCR_CACHE FUTUREUSE
+  if read -r ID NAME DESCRIPTION LOGO THEME AUTHORS DESCR_CACHE FUTUREUSE; then
+    vid_db="${_DATA}/${ID}/videos.db"
+    [ "$NAME" = \\ ] && NAME="(UNNAMED CHANNEL)"
+    cat <<-EOF
+       [div .channel
+         [h2 [a href="${_BASE}/channel/${ID}/" $(UNSTRING "${NAME}" |HTML)]]
+         [div .description . $(UNSTRING "$DESCR_CACHE")]
+         $( [ -f "$vid_db" -a -r "$vid_db" ] \
+            && while w_video "$ID"; do :; done <"$vid_db"
+         )
+       ]
+       EOF
+  else
+    return 1
+  fi
+}
+
+w_channel_list(){
+  if [ $USER_ID ]; then
+    printf '
+    [form .channel .newchannel method=POST
+      [hidden "channel" "%s"]
+      [submit "action" "newchannel" New Channel]
+    ]' "$(timeid)"
+  fi
+  [ -f "$chan_db" -a -r "$chan_db" ] \
+  && while w_channel; do :; done <"$chan_db"
+}
+
+if [ "$channel" -a "$video" ]; then
+  . ${_EXEC}/page_video.sh
+elif [ "$channel" -a "$action" = edit ]; then
+  [ "$USER_ID" -a ! "${CHANNEL_AUTHORS##*${USER_ID}*}" ] \
+  || REDIRECT "${_BASE}/${channel}/#ERROR_EDIT_NOTALLOWED"
+  yield_page "$CHANNEL_NAME - Edit" "channel edit" <<-EOF
+       [form .channel .edit method=POST
+         [input name="name" value="$(HTML "$CHANNEL_NAME")" placeholder="Channel Name"]
+         [textarea name="description" placeholder="Description" . $(HTML "$CHANNEL_DESCRIPTION")]
+         [submit "action" "update_channel" . Update]
+         [submit "action" "update_channel_cancel" . Cancel]
+       ]
+       EOF
+elif [ "$channel" ]; then
+  yield_page "$CHANNEL_NAME" "channel" <<-EOF
+       [nav [a href="../" Channels] - [span $(HTML "${CHANNEL_NAME:-(Unnamed Channel)}")]
+         $( [ "$USER_ID" -a ! "${CHANNEL_AUTHORS##*${USER_ID}*}" ] \
+             && printf ' - [a href="edit" edit]'
+          )
+       ]
+       [h1 .name $(HTML "$CHANNEL_NAME")]
+       [div .description . ${CHANNEL_DESCR_CACHE}]
+       [h1 .videos Videos]
+       [div .videos
+         $( [ "$USER_ID" -a ! "${CHANNEL_AUTHORS##*${USER_ID}*}" ] \
+            && printf '
+                [form .video .newvideo method=POST
+                  [hidden "video" "%s"]
+                  [submit "action" "newvideo" New Video]
+                ]' "$(timeid)"
+          )
+         $( [ -f "$vid_db" -a -r "$vid_db" ] \
+            && while w_video "$ID"; do :; done <"$vid_db"
+         )
+       ]
+       EOF
+else
+  yield_page "Channels" "channels" <<-EOF
+       $(w_channel_list)
+       EOF
+fi
diff --git a/page_video.sh b/page_video.sh
new file mode 100644 (file)
index 0000000..eca7be7
--- /dev/null
@@ -0,0 +1,94 @@
+#!/bin/sh
+
+if [ "$video" -a -f "$vid_db" -a -r "$vid_db" ]; then
+  read -r VIDEO_ID VIDEO_NAME VIDEO_DESCRIPTION VIDEO_RESX VIDEO_RESY \
+          VIDEO_LENGTH VIDEO_COVER VIDEO_STATUS VIDEO_UPLOADER VIDEO_HITS \
+          VIDEO_DESCR_CACHE VIDEO_FUTUREUSE <<-EOF
+       $(grep "^${video}       " "${vid_db}")
+       EOF
+  if [ "$VIDEO_ID" ]; then
+           VIDEO_NAME="$(UNSTRING "$VIDEO_NAME")"
+    VIDEO_DESCRIPTION="$(UNSTRING "$VIDEO_DESCRIPTION")"
+    VIDEO_DESCR_CACHE="$(UNSTRING "$VIDEO_DESCR_CACHE")"
+  else
+    video=''
+  fi
+fi
+
+# Video
+# ID   NAME    DESCRIPTION     RESX    RESY    LENGTH  COVER   STATUS (void|private|hidden|public)     UPLOADER        HITS    DESCR_CACHE     FUTUREUSE
+
+update_video(){
+  local id="${1}" name="${2}" description="${3}" resx="${4}" resy="${5}" \
+        length="${6}" cover="${7}" status="${8}" uploader="${9}" \
+        hits="${10}" descr_cache="${11}" futureuse="${12}"
+  local ID INFO
+
+  if LOCK "$vid_db"; then
+    while read -r ID INFO; do
+      if [ "$id" = "$ID" ]; then
+                # ID   NAME    DESCRIPTION RESX RESY   LENGTH  COVER   STATUS  UPLOADER HITS   DESCR_CACHE     FUTUREUSE
+        printf '%s     %s      %s      %i      %i      %i      %s      %s      %s      %i      %s      %s\n' \
+               "$id" "$(STRING "$name")" "$(STRING "$description")" "$resx" "$resy" "$length" \
+               "$(STRING "$cover")" "${status:-void}" "${uploader:-\\}" "$hits" \
+               "$(printf %s "$description" |markdown |STRING)" "${futureuse:-\\}"
+      else
+        printf '%s      %s\n' "$ID" "$INFO"
+      fi
+    done <"$vid_db" >"${vid_db}.$$"
+    mv -- "${vid_db}.$$" "${vid_db}"
+    RELEASE "$vid_db"
+  else
+    return 1
+  fi
+}
+
+
+[ "$REQUEST_METHOD" = POST ] && case "$(POST action)" in
+  update_video)
+    if [ ! "$USER_ID" ]; then
+      REDIRECT "${_BASE}/channel/${channel}/${video}/#ERROR_NOTLOGGEDIN"
+    elif [ "${CHANNEL_AUTHORS##*${USER_ID}*}" ]; then
+      REDIRECT "${_BASE}/channel/${channel}/${video}/#ERROR_UPDATE_NOTALLOWED"
+    elif update_video "$video" "$(POST name)" "$(POST description)" 0 0 0 \
+                      "" "void" "$USER_ID" 0 ""; then
+      REDIRECT "${_BASE}/channel/${channel}/${video}/#UPDATE_SUCCESS"
+    else
+      REDIRECT "${_BASE}/channel/${channel}/${video}/#ERROR_UPDATE_NOLOCK"
+    fi
+    ;;
+  update_video_cancel)
+    REDIRECT "${_BASE}/channel/${channel}/${video}/#CANCELED"
+    ;;
+esac
+
+if [ "$channel" -a "$video" -a "$action" = edit ]; then
+  yield_page "$VIDEO_NAME - Edit" "video edit" <<-EOF
+       [form .video .edit method=POST
+         [input name="name" value="$(HTML "$VIDEO_NAME")" placeholder="Video Name"]
+         [fieldset .status $([ $VIDEO_STATUS = void ] && printf "disabled=disabled")
+           [radio "status" "void" #status_private $(checked $VIDEO_STATUS private void)]
+             [label for=status_private tooltip="Video is only visible to channel authors" Private]
+           [radio "status" "void" #status_hidden  $(checked $VIDEO_STATUS hidden)]
+             [label for=status_hidden tooltip="Video will not be listed but can be viewed by anyone knowing the URL" Hidden]
+           [radio "status" "void" #status_public $(checked $VIDEO_STATUS public)]
+             [label for=status_public tooltip="Video will be listed publicly" Public]
+         ]
+         [textarea name="description" placeholder="Description" . $(HTML "$VIDEO_DESCRIPTION")]
+         [submit "action" "update_video" . Update]
+         [submit "action" "update_video_cancel" . Cancel]
+       ]
+       EOF
+elif [ "$channel" -a "$video" ]; then
+  yield_page "$VIDEO_NAME" "video edit" <<-EOF
+       [nav [a href="../../" Channels] - [a href="../" $(HTML "${CHANNEL_NAME:-(Unnamed Channel)}")] - [span $(HTML "${VIDEO_NAME:-(Unnamed Video)}")]
+         $( [ "$USER_ID" -a ! "${CHANNEL_AUTHORS##*${USER_ID}*}" ] \
+            && printf ' - [a href="edit" edit]'
+         )
+       ]
+       [h1 .name $(HTML "$VIDEO_NAME")]
+       [div .description . ${VIDEO_DESCR_CACHE}]
+       EOF
+else
+  . "$_EXEC/page_404.sh"
+fi
diff --git a/rawnet.css b/rawnet.css
new file mode 100644 (file)
index 0000000..c397b2d
--- /dev/null
@@ -0,0 +1,130 @@
+body {
+  background-position: right;
+  background-size: 4pt 4pt;
+  background-image: /* #6AF #6FF */
+    linear-gradient( 0deg, transparent 25%, rgba(128,128,128,.5) 25% 50%, transparent 50% 75%, rgba(192,192,192,.5) 75%),
+    linear-gradient(90deg, transparent 25%, rgba(128,128,128,.5) 25% 50%, transparent 50% 75%, rgba(192,192,192,.5) 75%);
+}
+
+header {
+  background: inherit;
+  padding: .25em 12em;
+  text-align: center;
+  box-shadow: #000 .25em .25em .25em;
+  z-index: 1;
+}
+
+header > * { background: inherit; }
+header:before,
+header > *:before {
+  content: ''; position: absolute;
+  top: 0; right: 0; bottom: 0; left: 0;
+  background-color: rgba(0,0,0,.75);
+}
+
+header a { color: #8CE; }
+
+header #user_login {
+  position: absolute;
+  right: 0; top: 31pt; max-height: 0;
+  width: 12em;
+  padding: 0 .5em;
+  text-align: center;
+  box-shadow: inherit;
+  transition: max-height linear .125s;
+}
+
+#user_login > * {
+  position: relative;
+  top: -2.5em;
+}
+#user_login > *:last-child {
+  margin-bottom: -2em;
+}
+header #user_login:hover {
+  max-height: 10em;
+}
+header #user_login > p {
+  color: #EEE;
+  font-size: .875em;
+  line-height: 1.125em;
+}
+header #user_login > p span {
+  display: block;
+  font-size: initial;
+  line-height: 1.375em;
+}
+header #user_login label {
+  top: -1.5em;
+  font-size: 1.25em;
+  text-decoration: underline;
+  padding-bottom: 1em;
+  color: #EEE;
+  text-align: right;
+}
+header #user_login > * {
+  display: none;
+}
+header #user_login > :first-child,
+header #user_login:hover > * {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+header #user_login:hover > a[href$="/register/"] {
+  text-align: right;
+  margin-top: .75em;
+}
+
+main {
+  background-color: rgba(255,255,255,.75);
+  margin: 1em; padding: 1em;
+  box-shadow: #000 .125em .125em 1em;
+}
+
+main nav {
+  font-size: .875em;
+  margin-top: -1em;
+}
+main nav > * {
+  padding: .125em .5em;
+  font-style: italic;
+  text-decoration: underline;
+}
+
+body.channel main h1.name {
+  text-align: center;
+}
+body.channel main .description,
+body.channel main form.edit,
+body.video   main form.edit {
+  max-width: 40em;
+  margin: auto;
+}
+
+body.video main form.edit input[name=name],
+body.video main form.edit textarea[name=description],
+body.channel main form.edit input[name=name],
+body.channel main form.edit textarea[name=description] {
+  display: block;
+  width: 100%;
+  margin-bottom: .5em;
+}
+
+body.channels main .channel {
+  border: 1pt solid;
+  border-radius: 4pt;
+  padding: .5em;
+  margin-bottom: .5em;
+}
+
+body.channels main .channel > h2,
+body.channels main .channel > .description {
+  margin: 0;
+  width: 140pt;
+  background-color: #FFF;
+}
+
+body.channel .videos .video {
+  display: inline-block;
+}