The main problem I was trying to work around was conditional GETs being issued for CSS files when I refreshed the page. I assumed this was because cache related headers weren't being set.
It looks like the headers are being set. A clean GET request does show appropriate headers for cache-control, etag, and expires.
It looks like I'm running into browser behavior. I'm not sure if this is standard, but the behavior somehwat matches the "User Invoked Refresh" behavior described here: http://blogs.msdn.com/b/ieinternals/archive/2010/07/08/technical-information-about-conditional-http-requests-and-the-refresh-button.aspx
If I use a browser reload, Chrome issues a conditonal GET for CSS. In this case, some of the cache headers are not present, which was what made me think the problem was server side initially.
If I go to the URL bar and hit ENTER, Chrome will fetch the CSS purely from cache.
The only solution I can think of is to use AppCache.