diff --git wc-post-types/inc/schedule.php wc-post-types/inc/schedule.php
new file mode 100644
index 0000000..ba54946
--- /dev/null
+++ wc-post-types/inc/schedule.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace WordCamp\Post_Types\Schedule;
+
+defined( 'WPINC' ) || die();
+
+
+/*
+ * todo
+ * make sure preserve all the functionality of the existing
+ * implement all the functionality of the new
+ * add length to session ui/save
+ * test and make compatible w/ personalized schedule builder
+ * give people working on https://meta.trac.wordpress.org/ticket/3306 and https://trello.com/c/kLVDRj5s/56-wordcamp-design-templates a heads up about this, since it might be best if they design default styles for it rather than old schedule
+ * test all the scenarios listed at https://meta.trac.wordpress.org/ticket/3117#comment:2. lightning talks too, if not already covered there.
+ * once ready for testing, deploy behind a filter
+ * once ready for all sites, enable feature flag
+ */
+
+
+/**
+ * Render the `[schedule]` shortcode using markup and styles for a CSS Grid-based approach.
+ *
+ * @param array $attributes
+ * // todo add extra params
+ *
+ *
+ * @return string
+ */
+function render_grid_schedule( $attributes, $tracks, $sessions ) {
+	$time_format = get_option( 'time_format', 'g:i a' );
+
+	ob_start();
+	require_once( dirname( __DIR__ ) . '/views/grid-schedule.php' );
+	return ob_get_clean();
+}
diff --git wc-post-types/views/grid-schedule.php wc-post-types/views/grid-schedule.php
new file mode 100644
index 0000000..7653e3f
--- /dev/null
+++ wc-post-types/views/grid-schedule.php
@@ -0,0 +1,250 @@
+<?php
+
+namespace WordCamp\Post_Types\Schedule;
+
+defined( 'WPINC' ) || die();
+
+/**
+ * todo
+ *
+ * @var array  $attributes
+ * @var array  $tracks
+ * @var array  $sessions
+ * @var array  $columns
+ * @var string $time_format
+ */
+
+/*
+ * todo
+ *
+ * move styles out of inline properties
+ * move most css to external file, only put what needs to be generated inside <style>?
+ * rename classes to match v1 schedule, have wordcamp prefix
+ * want sensible default styles that will make schedule look good out of the box, but that each camp can override as they see fit
+ * outputting <style> outside <head> is technically invalid, but pretty sure all browsers support it and always will
+ * test with 0 tracks, 1 track, tracks with no sessions assigned to them, etc
+ * people will want to set the track sorting order. can do that in css probably, but easier way?
+ */
+
+?>
+
+<style>
+	/*************************
+	 * LAYOUT
+	 *************************/
+	body {
+		padding: 50px;
+		counter-reset: session;
+		max-width: 1100px;
+		margin: 0 auto;
+		line-height: 1.5;
+	}
+
+	.track-slot {
+		display: none;
+	}
+	.session {
+		margin-bottom:  1em;
+	}
+
+	@supports( display:grid ) {
+		@media screen and (min-width:700px) {
+			.schedule {
+				display: grid;
+			}
+
+			/* a background for the sticky tracks */
+			.schedule::after {
+				display: block;
+				content: '';
+				position: sticky;
+				top: 0;
+				grid-column: track-1 / -1;
+				grid-row: tracks;
+				z-index: 999;
+				background-color: rgba(255,255,255,.9);
+			}
+
+			.track-slot {
+				display: block;
+				padding: 10px 5px 5px;
+				position: sticky;
+				top: 0; /* otherwise seeing a gap above in at least Firefox. */
+				z-index: 1000;
+			}
+			.session {
+				margin: 0;
+			}
+		}
+	}
+
+	.schedule {
+		grid-gap: 1em;
+		grid-template-rows:
+			[tracks] auto
+			<?php
+			foreach ( $sessions as $time_slot => $session_ids ) : ?>
+				[time-<?php echo esc_attr( date( 'Hi', $time_slot ) ); ?>] 1fr
+			<?php endforeach; ?>
+
+		 /* Note 1:
+			In this format, gridlines will need to be "named" in 24hr time
+
+			Note 2: Use "auto" instead of "1fr" for a more compact schedule where height of a slot is not proportional to the session length. Implementing a "compact" shortcode attribute might make sense for this!
+		*/
+		;
+		grid-template-columns:
+			[times] 4em
+			[track-<?php echo esc_html( $tracks[0]->slug ); ?>-start] 1fr
+			<?php for ( $i = 0; $i < count( $tracks ) - 1; $i++ ) : ?>
+				[track-<?php echo esc_html( $tracks[ $i ]->slug ); ?>-end track-<?php echo esc_html( $tracks[ $i + 1 ]->slug ); ?>-start] 1fr
+			<?php endfor; ?>
+			[track-<?php echo esc_html( $tracks[ $i ]->slug ); ?>-end]
+		;
+	}
+
+	/*************************
+	 * VISUAL STYLES
+	 *************************/
+	.text {
+		max-width: 750px;
+		font-size: 18px;
+		margin: 0 auto 50px;
+	}
+
+	.meta {
+		color: #555;
+		font-style: italic;
+	}
+	.meta a {
+		color: #555;
+	}
+
+	hr {
+		margin: 40px 0;
+	}
+
+	.session {
+		padding: .5em;
+		border-radius: 2px;
+		font-size: 14px;
+	}
+
+	.title,
+	.time-slot {
+		margin: 0;
+		font-size: 1em;
+	}
+
+	body {
+		counter-reset: session-count;
+	}
+	.title::before {
+		counter-increment: session-count;
+		content: 'Session #' counter(session-count) ': ';
+	}
+
+	.track-slot,
+	.time-slot {
+		font-weight: bold;
+		font-size:.75em;
+	}
+
+	span {
+		display: block;
+	}
+
+	.track-many {
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		background: #ccc;
+		color: #000;
+	}
+
+	ins {
+		text-decoration: none;
+		background-color: #ddffdd;
+	}
+
+
+
+	/* these styles won't be in the patch, they'd be custom css for a site */
+	.track-robertson-auditorium-upstairs {
+		background-color: #1259B2;
+		color: #fff;
+	}
+
+	.track-fisher-banquet-room-downstairs {
+		background-color: #687f00;
+		color: #fff;
+	}
+
+	.track-3 {
+		background-color: #544D69;
+		color: #fff;
+	}
+
+	.track-4 {
+		background-color: #c35500;
+		color: #fff;
+	}
+</style>
+
+<div class="schedule">
+	<?php foreach ( $tracks as $track ) : ?>
+		<span class="track-slot" aria-hidden="true" style="grid-column: track-<?php echo esc_attr( $track->slug ); ?>; grid-row: tracks;">
+			<?php echo esc_html( $track->name ); ?>
+		</span>
+	<?php endforeach; ?>
+
+	<?php foreach ( $sessions as $time_slot => $session_ids ) : ?>
+		<h2 class="time-slot" style="grid-column: times; grid-row: time-<?php echo esc_attr( date( 'Hi', $time_slot ) ); ?>;">
+			<?php echo esc_html( date_i18n( $time_format, $time_slot ) ); ?>
+		</h2>
+
+		<?php foreach ( $session_ids as $session_id ) : ?>
+			<?php
+
+			$session        = get_post( $session_id );
+			$speakers       = get_posts( array( 'post__in' => get_post_meta( $session_id, '_wcpt_speaker_id' ) ) );
+			$end_time       = $time_slot + ( absint( $session->_wcpt_session_length ) * 60 );
+			$session_tracks = get_the_terms( $session->ID, 'wcb_track' );
+			$track_classes  = preg_filter( '/^/', 'track-', wp_list_pluck( $session_tracks, 'slug' ) );
+
+			?>
+
+			<div
+				class="session session-<?php echo esc_attr( $session->post_name ); ?> <?php echo esc_attr( implode( ' ', $track_classes ) ); ?>"
+				style="
+					grid-column: track-<?php echo esc_attr( $session_tracks[ 0 ]->slug ); ?>-start;
+					grid-row: time-<?php echo esc_attr( date( 'Hi', $time_slot ) ); ?> / time-<?php echo esc_attr( date( 'Hi', $end_time ) ); ?>;
+				"
+			>
+				<h3 class="title">
+					<?php echo esc_html( $session->post_title ); ?>
+				</h3>
+
+				<span class="time">
+					<?php echo esc_html( date_i18n( $time_format, $time_slot ) ); ?>
+					-
+					<?php echo esc_html( date_i18n( $time_format, $end_time ) ); ?>
+				</span>
+
+				<span class="track">
+					<?php echo esc_attr( implode( ', ', wp_list_pluck( $session_tracks, 'name' ) ) ); ?>
+				</span>
+
+				<?php if ( $speakers ) : ?>
+					<ul class="author">
+						<?php foreach ( $speakers as $speaker ) : ?>
+							<li>
+								<?php echo esc_html( $speaker->post_title ); ?>
+							</li>
+						<?php endforeach; ?>
+					</ul>
+				<?php endif; ?>
+			</div>
+		<?php endforeach; ?>
+	<?php endforeach; ?>
+</div>
diff --git wc-post-types/wc-post-types.php wc-post-types/wc-post-types.php
index cc9e84c..fbb88ac 100644
--- wc-post-types/wc-post-types.php
+++ wc-post-types/wc-post-types.php
@@ -4,6 +4,8 @@
  * Plugin Description: Sessions, Speakers, Sponsors and much more.
  */
 
+use WordCamp\Post_Types\Schedule;
+
 require( 'inc/back-compat.php' );
 require_once( 'inc/favorite-schedule-shortcode.php' );
 
@@ -518,12 +520,12 @@ class WordCamp_Post_Types_Plugin {
 	}
 
 	/**
-	 * The [schedule] shortcode callback (experimental)
+	 * Render the appropriate version of the `[schedule]` shortcode.
 	 *
-	 * @todo implement date arg
-	 * @todo implement anchor for session_link
-	 * @todo maybe simplify $attr['custom']
-	 * @todo cleanup
+	 * @param array  $attr
+	 * @param string $content
+	 *
+	 * @return string
 	 */
 	function shortcode_schedule( $attr, $content ) {
 		$this->enqueue_schedule_shortcode_dependencies();
@@ -532,8 +534,29 @@ class WordCamp_Post_Types_Plugin {
 		$tracks                      = get_schedule_tracks( $attr['tracks'] );
 		$tracks_explicitly_specified = 'all' !== $attr['tracks'];
 		$sessions                    = get_schedule_sessions( $attr['date'], $tracks_explicitly_specified, $tracks );
-		$columns                     = get_schedule_columns( $tracks, $sessions, $tracks_explicitly_specified );
 
+		if ( wcorg_skip_feature( 'css-grid-schedule' ) ) {
+			$columns = get_schedule_columns( $tracks, $sessions, $tracks_explicitly_specified );
+			$output  = $this->render_table_schedule( $attr, $content, $tracks, $sessions, $columns );
+		} else {
+			require_once( __DIR__ . '/inc/schedule.php' );
+			$output = Schedule\render_grid_schedule( $attr, $tracks, $sessions );
+		}
+
+		$output .= $this->fav_session_email_form();
+
+		return $output;
+	}
+
+	/**
+	 * The [schedule] shortcode callback (experimental)
+	 *
+	 * @todo implement date arg
+	 * @todo implement anchor for session_link
+	 * @todo maybe simplify $attr['custom']
+	 * @todo cleanup
+	 */
+	function render_table_schedule( $attr, $content, $tracks, $sessions, $columns ) {
 		$html  = '<table class="wcpt-schedule" border="0">';
 		$html .= '<thead>';
 		$html .= '<tr>';
@@ -704,7 +727,7 @@ class WordCamp_Post_Types_Plugin {
 
 		$html .= '</tbody>';
 		$html .= '</table>';
-		$html .= $this->fav_session_email_form();
+
 		return $html;
 	}
 
@@ -1734,6 +1757,7 @@ class WordCamp_Post_Types_Plugin {
 		$session_hours    = ( $session_time ) ? date( 'g', $session_time )     : date( 'g' );
 		$session_minutes  = ( $session_time ) ? date( 'i', $session_time )     : '00';
 		$session_meridiem = ( $session_time ) ? date( 'a', $session_time )     : 'am';
+		$session_length   = absint( get_post_meta( $post->ID, '_wcpt_session_length', true ) );	// todo default to 30 minutes or 45 ?
 		$session_type     = get_post_meta( $post->ID, '_wcpt_session_type', true );
 		$session_slides   = get_post_meta( $post->ID, '_wcpt_session_slides', true );
 		$session_video    = get_post_meta( $post->ID, '_wcpt_session_video',  true );
@@ -1744,6 +1768,7 @@ class WordCamp_Post_Types_Plugin {
 		<p>
 			<label for="wcpt-session-date"><?php _e( 'Date:', 'wordcamporg' ); ?></label>
 			<input type="text" id="wcpt-session-date" data-date="<?php echo esc_attr( $session_date ); ?>" name="wcpt-session-date" value="<?php echo esc_attr( $session_date ); ?>" /><br />
+
 			<label><?php _e( 'Time:', 'wordcamporg' ); ?></label>
 
 			<select name="wcpt-session-hour" aria-label="<?php _e( 'Session Start Hour', 'wordcamporg' ); ?>">
@@ -1765,7 +1790,22 @@ class WordCamp_Post_Types_Plugin {
 			<select name="wcpt-session-meridiem" aria-label="<?php _e( 'Session Meridiem', 'wordcamporg' ); ?>">
 				<option value="am" <?php selected( 'am', $session_meridiem ); ?>>am</option>
 				<option value="pm" <?php selected( 'pm', $session_meridiem ); ?>>pm</option>
-			</select>
+			</select><br />
+
+			<label for="wcpt-session-length">
+				<?php
+				// translators: todo
+				esc_html_e( 'Length:', 'wordcamporg' );
+				?>
+
+				<!-- todo dropdown for every 5 minutes instead of text input? what about a full-day workshop that's ~7 hours? what if it has a break in the middle for lunch? -->
+				<input type="number" name="wcpt-session-length" value="<?php echo esc_attr( $session_length ); ?>" />
+				<?php
+				// translators: todo
+				esc_html_e( 'minutes', 'wordcamporg' );
+				?>
+			</label>
+			<br />
 		</p>
 
 		<p>
@@ -1964,6 +2004,9 @@ class WordCamp_Post_Types_Plugin {
 			) );
 			update_post_meta( $post_id, '_wcpt_session_time', $session_time );
 
+			$session_length = absint( $_POST['wcpt-session-length'] );
+			update_post_meta( $post_id, '_wcpt_session_length', $session_length );
+
 			// Update session type
 			$session_type = sanitize_text_field( $_POST['wcpt-session-type'] );
 			if ( ! in_array( $session_type, array( 'session', 'custom' ) ) ) {
